Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,64 @@
import Foundation
import Postbox
import TelegramApi
extension UnauthorizedAccountTermsOfService {
init?(apiTermsOfService: Api.help.TermsOfService) {
switch apiTermsOfService {
case let .termsOfService(_, id, text, entities, minAgeConfirm):
let idData: String
switch id {
case let .dataJSON(data):
idData = data
}
self.init(id: idData, text: text, entities: messageTextEntitiesFromApiEntities(entities), ageConfirmation: minAgeConfirm)
}
}
}
extension SentAuthorizationCodeType {
init(apiType: Api.auth.SentCodeType) {
switch apiType {
case let .sentCodeTypeApp(length):
self = .otherSession(length: length)
case let .sentCodeTypeSms(length):
self = .sms(length: length)
case let .sentCodeTypeCall(length):
self = .call(length: length)
case let .sentCodeTypeFlashCall(pattern):
self = .flashCall(pattern: pattern)
case let .sentCodeTypeMissedCall(prefix, length):
self = .missedCall(numberPrefix: prefix, length: length)
case let .sentCodeTypeEmailCode(flags, emailPattern, length, resetAvailablePeriod, resetPendingDate):
self = .email(emailPattern: emailPattern, length: length, resetAvailablePeriod: resetAvailablePeriod, resetPendingDate: resetPendingDate, appleSignInAllowed: (flags & (1 << 0)) != 0, setup: false)
case let .sentCodeTypeSetUpEmailRequired(flags):
self = .emailSetupRequired(appleSignInAllowed: (flags & (1 << 0)) != 0)
case let .sentCodeTypeFragmentSms(url, length):
self = .fragment(url: url, length: length)
case let .sentCodeTypeFirebaseSms(_, _, _, _, _, pushTimeout, length):
self = .firebase(pushTimeout: pushTimeout, length: length)
case let .sentCodeTypeSmsWord(_, beginning):
self = .word(startsWith: beginning)
case let .sentCodeTypeSmsPhrase(_, beginning):
self = .phrase(startsWith: beginning)
}
}
}
extension AuthorizationCodeNextType {
init(apiType: Api.auth.CodeType) {
switch apiType {
case .codeTypeSms:
self = .sms
case .codeTypeCall:
self = .call
case .codeTypeFlashCall:
self = .flashCall
case .codeTypeMissedCall:
self = .missedCall
case .codeTypeFragmentSms:
self = .fragment
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,184 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
import TelegramApi
final class AccountTaskManager {
private final class Impl {
private let queue: Queue
private let accountPeerId: PeerId
private let stateManager: AccountStateManager
private let accountManager: AccountManager<TelegramAccountManagerTypes>
private let networkArguments: NetworkInitializationArguments
private let viewTracker: AccountViewTracker
private let mediaReferenceRevalidationContext: MediaReferenceRevalidationContext
private let isMainApp: Bool
private let testingEnvironment: Bool
private var stateDisposable: Disposable?
private let tasksDisposable = MetaDisposable()
private let configurationDisposable = MetaDisposable()
private let promoDisposable = MetaDisposable()
private let managedTopReactionsDisposable = MetaDisposable()
private var isUpdating: Bool = false
init(queue: Queue, accountPeerId: PeerId, stateManager: AccountStateManager, accountManager: AccountManager<TelegramAccountManagerTypes>,
networkArguments: NetworkInitializationArguments, viewTracker: AccountViewTracker, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, isMainApp: Bool, testingEnvironment: Bool) {
self.queue = queue
self.accountPeerId = accountPeerId
self.stateManager = stateManager
self.accountManager = accountManager
self.networkArguments = networkArguments
self.viewTracker = viewTracker
self.mediaReferenceRevalidationContext = mediaReferenceRevalidationContext
self.isMainApp = isMainApp
self.testingEnvironment = testingEnvironment
stateManager.isPremiumUpdated = { [weak self] in
guard let self = self else {
return
}
if !self.isUpdating {
self.managedTopReactionsDisposable.set(managedTopReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
}
}
self.stateDisposable = (stateManager.isUpdating
|> filter { !$0 }
|> take(1)
|> deliverOn(self.queue)).start(next: { [weak self] value in
guard let self = self else {
return
}
self.stateManagerUpdated(isUpdating: value)
})
}
private func stateManagerUpdated(isUpdating: Bool) {
self.isUpdating = isUpdating
if isUpdating {
self.tasksDisposable.set(nil)
self.managedTopReactionsDisposable.set(nil)
} else {
let tasks = DisposableSet()
if self.isMainApp {
tasks.add(managedSynchronizePeerReadStates(network: self.stateManager.network, postbox: self.stateManager.postbox, stateManager: self.stateManager).start())
tasks.add(managedSynchronizeGroupMessageStats(network: self.stateManager.network, postbox: self.stateManager.postbox, stateManager: self.stateManager).start())
tasks.add(managedGlobalNotificationSettings(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedSynchronizePinnedChatsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId, stateManager: self.stateManager, tag: OperationLogTags.SynchronizePinnedChats).start())
tasks.add(managedSynchronizePinnedChatsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId, stateManager: self.stateManager, tag: OperationLogTags.SynchronizePinnedSavedChats).start())
tasks.add(managedSynchronizeGroupedPeersOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager, namespace: .stickers).start())
tasks.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager, namespace: .masks).start())
tasks.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager, namespace: .emoji).start())
tasks.add(managedSynchronizeMarkFeaturedStickerPacksAsSeenOperations(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedSynchronizeRecentlyUsedMediaOperations(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network, category: .stickers, revalidationContext: self.mediaReferenceRevalidationContext).start())
tasks.add(managedSynchronizeSavedGifsOperations(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network, revalidationContext: self.mediaReferenceRevalidationContext).start())
tasks.add(managedSynchronizeSavedStickersOperations(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network, revalidationContext: self.mediaReferenceRevalidationContext).start())
tasks.add(_internal_managedRecentlyUsedInlineBots(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start())
tasks.add(managedSynchronizeConsumeMessageContentOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedConsumePersonalMessagesActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedReadReactionActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedSynchronizeMarkAllUnseenReactionsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedApplyPendingMessageReactionsActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedApplyPendingMessageStarsReactionsActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedApplyPendingPaidMessageActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedApplyPendingScheduledMessagesActions(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedSynchronizeAvailableReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedSynchronizeAvailableMessageEffects(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedSynchronizeEmojiSearchCategories(postbox: self.stateManager.postbox, network: self.stateManager.network, kind: .emoji).start())
tasks.add(managedSynchronizeEmojiSearchCategories(postbox: self.stateManager.postbox, network: self.stateManager.network, kind: .status).start())
tasks.add(managedSynchronizeEmojiSearchCategories(postbox: self.stateManager.postbox, network: self.stateManager.network, kind: .avatar).start())
tasks.add(managedSynchronizeEmojiSearchCategories(postbox: self.stateManager.postbox, network: self.stateManager.network, kind: .combinedChatStickers).start())
tasks.add(managedSynchronizeAttachMenuBots(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network, force: true).start())
tasks.add(managedSynchronizeNotificationSoundList(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedChatListFilters(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start())
tasks.add(managedAnimatedEmojiUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedAnimatedEmojiAnimationsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedGenericEmojiEffects(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedGreetingStickers(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedPremiumStickers(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedAllPremiumStickers(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedRecentStatusEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedFeaturedStatusEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedFeaturedChannelStatusEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedUniqueStarGifts(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedProfilePhotoEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedGroupPhotoEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedBackgroundIconEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedRecentReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedDefaultTagReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(_internal_loadedStickerPack(postbox: self.stateManager.postbox, network: self.stateManager.network, reference: .iconStatusEmoji, forceActualized: true).start())
tasks.add(_internal_loadedStickerPack(postbox: self.stateManager.postbox, network: self.stateManager.network, reference: .iconChannelStatusEmoji, forceActualized: true).start())
tasks.add(managedDisabledChannelStatusIconEmoji(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(_internal_loadedStickerPack(postbox: self.stateManager.postbox, network: self.stateManager.network, reference: .iconTopicEmoji, forceActualized: true).start())
tasks.add(managedPeerColorUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedStarGiftsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start())
tasks.add(managedSavedMusicIdsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start())
self.managedTopReactionsDisposable.set(managedTopReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
//tasks.add(managedVoipConfigurationUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
self.reloadAppConfiguration()
tasks.add(managedPremiumPromoConfigurationUpdates(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedAutodownloadSettingsUpdates(accountManager: self.accountManager, network: self.stateManager.network).start())
tasks.add(managedTermsOfServiceUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedAppUpdateInfo(network: self.stateManager.network, stateManager: self.stateManager).start())
tasks.add(managedLocalizationUpdatesOperations(accountManager: self.accountManager, postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedPendingPeerNotificationSettings(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedSynchronizeAppLogEventsOperations(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedNotificationSettingsBehaviors(postbox: self.stateManager.postbox).start())
tasks.add(managedThemesUpdates(accountManager: self.accountManager, postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedContactBirthdays(stateManager: self.stateManager).start())
if !self.testingEnvironment {
tasks.add(managedChatThemesUpdates(accountManager: self.accountManager, network: self.stateManager.network).start())
}
}
self.tasksDisposable.set(tasks)
}
}
deinit {
self.stateDisposable?.dispose()
self.tasksDisposable.dispose()
self.configurationDisposable.dispose()
self.managedTopReactionsDisposable.dispose()
self.promoDisposable.dispose()
}
func reloadAppConfiguration() {
self.configurationDisposable.set(managedAppConfigurationUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
self.promoDisposable.set(managedPromoInfoUpdates(accountPeerId: self.accountPeerId, postbox: self.stateManager.postbox, network: self.stateManager.network, viewTracker: self.viewTracker).start())
}
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
init(stateManager: AccountStateManager, accountManager: AccountManager<TelegramAccountManagerTypes>,
networkArguments: NetworkInitializationArguments, viewTracker: AccountViewTracker, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, isMainApp: Bool, testingEnvironment: Bool) {
let queue = Account.sharedQueue
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, accountPeerId: stateManager.accountPeerId, stateManager: stateManager, accountManager: accountManager, networkArguments: networkArguments, viewTracker: viewTracker, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, isMainApp: isMainApp, testingEnvironment: testingEnvironment)
})
}
func reloadAppConfiguration() {
self.impl.with { impl in
impl.reloadAppConfiguration()
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
import TelegramApi
func managedAppChangelog(postbox: Postbox, network: Network, stateManager: AccountStateManager, appVersion: String) -> Signal<Void, NoError> {
return .never()
}
@@ -0,0 +1,11 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
func updateAppChangelogState(transaction: Transaction, _ f: @escaping (AppChangelogState) -> AppChangelogState) {
transaction.updatePreferencesEntry(key: PreferencesKeys.appChangelogState, { current in
return PreferencesEntry(f((current?.get(AppChangelogState.self)) ?? AppChangelogState.default))
})
}
@@ -0,0 +1,17 @@
import Postbox
public func currentAppConfiguration(transaction: Transaction) -> AppConfiguration {
if let entry = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) {
return entry
} else {
return AppConfiguration.defaultValue
}
}
func updateAppConfiguration(transaction: Transaction, _ f: (AppConfiguration) -> AppConfiguration) {
let current = currentAppConfiguration(transaction: transaction)
let updated = f(current)
if updated != current {
transaction.setPreferencesEntry(key: PreferencesKeys.appConfiguration, value: PreferencesEntry(updated))
}
}
@@ -0,0 +1,48 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
import TelegramApi
public struct AppUpdateInfo: Equatable {
public let blocking: Bool
public let version: String
public let text: String
public let entities: [MessageTextEntity]
public init(blocking: Bool, version: String, text: String, entities: [MessageTextEntity]) {
self.blocking = blocking
self.version = version
self.text = text
self.entities = entities
}
}
extension AppUpdateInfo {
init?(apiAppUpdate: Api.help.AppUpdate) {
switch apiAppUpdate {
case let .appUpdate(flags, _, version, text, entities, _, _, _):
self.blocking = (flags & (1 << 0)) != 0
self.version = version
self.text = text
self.entities = messageTextEntitiesFromApiEntities(entities)
case .noAppUpdate:
return nil
}
}
}
func managedAppUpdateInfo(network: Network, stateManager: AccountStateManager) -> Signal<Never, NoError> {
let poll = network.request(Api.functions.help.getAppUpdate(source: ""))
|> retryRequest
|> mapToSignal { [weak stateManager] result -> Signal<Never, NoError> in
let updated = AppUpdateInfo(apiAppUpdate: result)
stateManager?.modifyAppUpdateInfo { _ in
return updated
}
return .complete()
}
return (poll |> then(.complete() |> suspendAwareDelay(12.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
@@ -0,0 +1,614 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
private func copyOrMoveResourceData(from fromResource: MediaResource, to toResource: MediaResource, mediaBox: MediaBox) {
if fromResource is CloudFileMediaResource || fromResource is CloudDocumentMediaResource || fromResource is SecretFileMediaResource {
mediaBox.copyResourceData(from: fromResource.id, to: toResource.id)
} else if let fromResource = fromResource as? LocalFileMediaResource, fromResource.isSecretRelated {
mediaBox.copyResourceData(from: fromResource.id, to: toResource.id)
} else {
mediaBox.moveResourceData(from: fromResource.id, to: toResource.id)
}
}
func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: Bool, skipPreviews: Bool = false) {
if let fromImage = from as? TelegramMediaImage, let toImage = to as? TelegramMediaImage {
let fromSmallestRepresentation = smallestImageRepresentation(fromImage.representations)
if let fromSmallestRepresentation = fromSmallestRepresentation, let toSmallestRepresentation = smallestImageRepresentation(toImage.representations) {
let leeway: Int32 = 4
let widthDifference = fromSmallestRepresentation.dimensions.width - toSmallestRepresentation.dimensions.width
let heightDifference = fromSmallestRepresentation.dimensions.height - toSmallestRepresentation.dimensions.height
if abs(widthDifference) < leeway && abs(heightDifference) < leeway {
copyOrMoveResourceData(from: fromSmallestRepresentation.resource, to: toSmallestRepresentation.resource, mediaBox: postbox.mediaBox)
}
}
if let fromLargestRepresentation = largestImageRepresentation(fromImage.representations), let toLargestRepresentation = largestImageRepresentation(toImage.representations) {
if fromLargestRepresentation.resource is CloudPeerPhotoSizeMediaResource {
} else {
copyOrMoveResourceData(from: fromLargestRepresentation.resource, to: toLargestRepresentation.resource, mediaBox: postbox.mediaBox)
}
}
} else if let fromFile = from as? TelegramMediaFile, let toFile = to as? TelegramMediaFile {
if !skipPreviews {
if let fromPreview = smallestImageRepresentation(fromFile.previewRepresentations), let toPreview = smallestImageRepresentation(toFile.previewRepresentations) {
copyOrMoveResourceData(from: fromPreview.resource, to: toPreview.resource, mediaBox: postbox.mediaBox)
}
if let fromVideoThumbnail = fromFile.videoThumbnails.first, let toVideoThumbnail = toFile.videoThumbnails.first, fromVideoThumbnail.resource.id != toVideoThumbnail.resource.id {
copyOrMoveResourceData(from: fromVideoThumbnail.resource, to: toVideoThumbnail.resource, mediaBox: postbox.mediaBox)
}
}
let videoFirstFrameFromPath = postbox.mediaBox.cachedRepresentationCompletePath(fromFile.resource.id, keepDuration: .general, representationId: "first-frame")
let videoFirstFrameToPath = postbox.mediaBox.cachedRepresentationCompletePath(toFile.resource.id, keepDuration: .general, representationId: "first-frame")
if FileManager.default.fileExists(atPath: videoFirstFrameFromPath) {
let _ = try? FileManager.default.copyItem(atPath: videoFirstFrameFromPath, toPath: videoFirstFrameToPath)
}
if (force || fromFile.size == toFile.size || fromFile.resource.size == toFile.resource.size) && fromFile.mimeType == toFile.mimeType {
copyOrMoveResourceData(from: fromFile.resource, to: toFile.resource, mediaBox: postbox.mediaBox)
}
} else if let fromPaidContent = from as? TelegramMediaPaidContent, let toPaidContent = to as? TelegramMediaPaidContent {
for (fromMedia, toMedia) in zip(fromPaidContent.extendedMedia, toPaidContent.extendedMedia) {
if case let .full(fullFromMedia) = fromMedia, case let .full(fullToMedia) = toMedia {
applyMediaResourceChanges(from: fullFromMedia, to: fullToMedia, postbox: postbox, force: force)
}
}
}
}
func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, message: Message, cacheReferenceKey: CachedSentMediaReferenceKey?, result: Api.Updates, accountPeerId: PeerId, pendingMessageEvent: @escaping (PeerPendingMessageDelivered) -> Void) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
let messageId: Int32?
var apiMessage: Api.Message?
var correspondingMessageId: Int32?
for update in result.allUpdates {
switch update {
case let .updateMessageID(id, randomId):
for attribute in message.attributes {
if let attribute = attribute as? OutgoingMessageInfoAttribute {
if attribute.uniqueId == randomId {
correspondingMessageId = id
break
}
}
}
default:
break
}
}
for resultMessage in result.messages {
if let id = resultMessage.id() {
if let correspondingMessageId = correspondingMessageId {
if id.id != correspondingMessageId {
continue
}
}
if id.peerId == message.id.peerId {
apiMessage = resultMessage
break
}
}
}
if let apiMessage = apiMessage, let id = apiMessage.id() {
messageId = id.id
} else {
messageId = result.rawMessageIds.first
}
var updatedTimestamp: Int32?
if let apiMessage = apiMessage {
switch apiMessage {
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
updatedTimestamp = date
case .messageEmpty:
break
case let .messageService(_, _, _, _, _, _, date, _, _, _):
updatedTimestamp = date
}
} else {
switch result {
case let .updateShortSentMessage(_, _, _, _, date, _, _, _):
updatedTimestamp = date
default:
break
}
}
let channelPts = result.channelPts
var sentStickers: [TelegramMediaFile] = []
var sentGifs: [TelegramMediaFile] = []
if let updatedTimestamp {
transaction.offsetPendingMessagesTimestamps(lowerBound: message.id, excludeIds: Set([message.id]), timestamp: updatedTimestamp)
}
var updatedMessage: StoreMessage?
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
transaction.updateMessage(message.id, update: { currentMessage in
let media: [Media]
var attributes: [MessageAttribute]
let text: String
let forwardInfo: StoreMessageForwardInfo?
let threadId: Int64?
var namespace = Namespaces.Message.Cloud
if message.id.namespace == Namespaces.Message.ScheduledLocal {
namespace = Namespaces.Message.ScheduledCloud
}
if let apiMessage = apiMessage, let apiMessagePeerId = apiMessage.peerId, let updatedMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: accountPeerId, peerIsForum: transaction.getPeer(apiMessagePeerId)?.isForumOrMonoForum ?? false, namespace: namespace) {
media = updatedMessage.media
attributes = updatedMessage.attributes
text = updatedMessage.text
forwardInfo = updatedMessage.forwardInfo
threadId = updatedMessage.threadId
} else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, ttlPeriod) = result {
let (mediaValue, _, nonPremium, hasSpoiler, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId)
if let mediaValue = mediaValue {
media = [mediaValue]
} else {
media = []
}
var updatedAttributes: [MessageAttribute] = currentMessage.attributes
if let entities = entities, !entities.isEmpty {
for i in 0 ..< updatedAttributes.count {
if updatedAttributes[i] is TextEntitiesMessageAttribute {
updatedAttributes.remove(at: i)
break
}
}
updatedAttributes.append(TextEntitiesMessageAttribute(entities: messageTextEntitiesFromApiEntities(entities)))
}
updatedAttributes = updatedAttributes.filter({ !($0 is AutoremoveTimeoutMessageAttribute) })
if let ttlPeriod = ttlPeriod {
updatedAttributes.append(AutoremoveTimeoutMessageAttribute(timeout: ttlPeriod, countdownBeginTime: updatedTimestamp))
}
updatedAttributes = updatedAttributes.filter({ !($0 is NonPremiumMessageAttribute) })
if let nonPremium = nonPremium, nonPremium {
updatedAttributes.append(NonPremiumMessageAttribute())
}
if let hasSpoiler = hasSpoiler, hasSpoiler {
updatedAttributes.append(MediaSpoilerMessageAttribute())
}
for i in 0 ..< updatedAttributes.count {
if updatedAttributes[i] is OutgoingScheduleInfoMessageAttribute {
updatedAttributes.remove(at: i)
break
}
}
if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
for i in 0 ..< updatedAttributes.count {
if updatedAttributes[i] is OutgoingQuickReplyMessageAttribute {
updatedAttributes.remove(at: i)
break
}
}
}
attributes = updatedAttributes
text = currentMessage.text
forwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
threadId = currentMessage.threadId
} else {
media = currentMessage.media
attributes = currentMessage.attributes
text = currentMessage.text
forwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
threadId = currentMessage.threadId
}
let updatedId: MessageId
if let messageId = messageId {
var namespace: MessageId.Namespace = Namespaces.Message.Cloud
if attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) {
namespace = Namespaces.Message.ScheduledCloud
}
if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
namespace = Namespaces.Message.QuickReplyCloud
} else if let updatedTimestamp = updatedTimestamp {
if attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) {
namespace = Namespaces.Message.ScheduledCloud
} else {
if message.scheduleTime != nil && message.scheduleTime == updatedTimestamp {
namespace = Namespaces.Message.ScheduledCloud
}
}
} else if Namespaces.Message.allScheduled.contains(message.id.namespace) {
namespace = Namespaces.Message.ScheduledCloud
}
updatedId = MessageId(peerId: currentMessage.id.peerId, namespace: namespace, id: messageId)
} else {
updatedId = currentMessage.id
}
for attribute in currentMessage.attributes {
if let attribute = attribute as? OutgoingMessageInfoAttribute {
bubbleUpEmojiOrStickersets = attribute.bubbleUpEmojiOrStickersets
} else if let attribute = attribute as? OutgoingQuickReplyMessageAttribute {
if let threadId {
_internal_applySentQuickReplyMessage(transaction: transaction, shortcut: attribute.shortcut, quickReplyId: Int32(clamping: threadId))
}
}
}
if let channelPts = channelPts {
for i in 0 ..< attributes.count {
if let _ = attributes[i] as? ChannelMessageStateVersionAttribute {
attributes.remove(at: i)
break
}
}
attributes.append(ChannelMessageStateVersionAttribute(pts: channelPts))
}
if let fromMedia = currentMessage.media.first, let toMedia = media.first {
applyMediaResourceChanges(from: fromMedia, to: toMedia, postbox: postbox, force: false)
}
if forwardInfo == nil {
inner: for media in media {
if let file = media as? TelegramMediaFile {
for attribute in file.attributes {
switch attribute {
case let .Sticker(_, packReference, _):
if packReference != nil {
sentStickers.append(file)
}
case .Animated:
if !file.isAnimatedSticker && !file.isVideoSticker {
sentGifs.append(file)
}
default:
break
}
}
break inner
}
}
}
var entitiesAttribute: TextEntitiesMessageAttribute?
for attribute in attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entitiesAttribute = attribute
break
}
}
let (tags, globalTags) = tagsForStoreMessage(incoming: currentMessage.flags.contains(.Incoming), attributes: attributes, media: media, textEntities: entitiesAttribute?.entities, isPinned: currentMessage.tags.contains(.pinned))
if currentMessage.id.peerId.namespace == Namespaces.Peer.CloudChannel, !currentMessage.flags.contains(.Incoming), !Namespaces.Message.allNonRegular.contains(currentMessage.id.namespace) {
let peerId = currentMessage.id.peerId
if let peer = transaction.getPeer(peerId) {
if let peer = peer as? TelegramChannel {
inner: switch peer.info {
case let .group(info):
if info.flags.contains(.slowModeEnabled), peer.adminRights == nil && !peer.flags.contains(.isCreator) {
transaction.updatePeerCachedData(peerIds: [peerId], update: { peerId, current in
var cachedData = current as? CachedChannelData ?? CachedChannelData()
if let slowModeTimeout = cachedData.slowModeTimeout {
cachedData = cachedData.withUpdatedSlowModeValidUntilTimestamp(currentMessage.timestamp + slowModeTimeout)
return cachedData
} else {
return current
}
})
}
default:
break inner
}
}
}
}
let updatedMessageValue = StoreMessage(id: updatedId, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: updatedTimestamp ?? currentMessage.timestamp, flags: [], tags: tags, globalTags: globalTags, localTags: currentMessage.localTags, forwardInfo: forwardInfo, authorId: currentMessage.author?.id, text: text, attributes: attributes, media: media)
updatedMessage = updatedMessageValue
return .update(updatedMessageValue)
})
if let updatedMessage = updatedMessage, case let .Id(updatedId) = updatedMessage.id {
if message.id.namespace == Namespaces.Message.Local && updatedId.namespace == Namespaces.Message.Cloud && updatedId.peerId.namespace == Namespaces.Peer.CloudChannel {
if let threadId = updatedMessage.threadId {
if let authorId = updatedMessage.authorId {
updateMessageThreadStats(transaction: transaction, threadKey: MessageThreadKey(peerId: updatedMessage.id.peerId, threadId: threadId), removedCount: 0, addedMessagePeers: [ReplyThreadUserMessage(id: authorId, messageId: updatedId, isOutgoing: true)])
}
}
}
if updatedMessage.id.namespace == Namespaces.Message.Cloud, let cacheReferenceKey = cacheReferenceKey {
var storeMedia: Media?
var mediaCount = 0
for media in updatedMessage.media {
if let image = media as? TelegramMediaImage {
storeMedia = image
mediaCount += 1
} else if let file = media as? TelegramMediaFile {
storeMedia = file
mediaCount += 1
}
}
if mediaCount > 1 {
storeMedia = nil
}
if let storeMedia = storeMedia {
storeCachedSentMediaReference(transaction: transaction, key: cacheReferenceKey, media: storeMedia)
}
}
}
for file in sentStickers {
if let entry = CodableEntry(RecentMediaItem(file)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStickers, item: OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry), removeTailIfCountExceeds: 20)
}
}
for file in sentGifs {
if !file.hasLinkedStickers {
if let entry = CodableEntry(RecentMediaItem(file)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentGifs, item: OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry), removeTailIfCountExceeds: 200)
}
}
}
if !bubbleUpEmojiOrStickersets.isEmpty {
applyBubbleUpEmojiOrStickersets(transaction: transaction, ids: bubbleUpEmojiOrStickersets)
}
stateManager.addUpdates(result)
stateManager.addUpdateGroups([.ensurePeerHasLocalState(id: message.id.peerId)])
if let updatedMessage, case let .Id(id) = updatedMessage.id {
pendingMessageEvent(PeerPendingMessageDelivered(
id: id,
isSilent: updatedMessage.attributes.contains(where: { attribute in
if let attribute = attribute as? NotificationInfoMessageAttribute {
return attribute.flags.contains(.muted)
} else {
return false
}
}),
isPendingProcessing: updatedMessage.attributes.contains(where: { $0 is PendingProcessingMessageAttribute })
))
}
}
}
func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManager, messages: [Message], result: Api.Updates, pendingMessageEvents: @escaping ([PeerPendingMessageDelivered]) -> Void) -> Signal<Void, NoError> {
guard !messages.isEmpty else {
return .single(Void())
}
return postbox.transaction { transaction -> Void in
let updatedRawMessageIds = result.updatedRawMessageIds
var namespace = Namespaces.Message.Cloud
if Namespaces.Message.allQuickReply.contains(messages[0].id.namespace) {
namespace = Namespaces.Message.QuickReplyCloud
} else if let message = messages.first, let apiMessage = result.messages.first {
if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
namespace = Namespaces.Message.ScheduledCloud
} else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 {
namespace = Namespaces.Message.ScheduledCloud
}
}
var resultMessages: [MessageId: StoreMessage] = [:]
for apiMessage in result.messages {
var peerIsForum = false
if let apiMessagePeerId = apiMessage.peerId, let peer = transaction.getPeer(apiMessagePeerId) {
if peer.isForumOrMonoForum {
peerIsForum = true
}
}
if let resultMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: stateManager.accountPeerId, peerIsForum: peerIsForum, namespace: namespace), case let .Id(id) = resultMessage.id {
resultMessages[id] = resultMessage
}
}
var mapping: [(Message, MessageIndex, StoreMessage)] = []
for message in messages {
var uniqueId: Int64?
inner: for attribute in message.attributes {
if let outgoingInfo = attribute as? OutgoingMessageInfoAttribute {
uniqueId = outgoingInfo.uniqueId
break inner
}
}
if let uniqueId = uniqueId {
if let updatedId = updatedRawMessageIds[uniqueId] {
if let storeMessage = resultMessages[MessageId(peerId: message.id.peerId, namespace: namespace, id: updatedId)], case let .Id(id) = storeMessage.id {
mapping.append((message, MessageIndex(id: id, timestamp: storeMessage.timestamp), storeMessage))
}
} else {
// assertionFailure()
}
} else {
assertionFailure()
}
}
mapping.sort { $0.1 < $1.1 }
let latestPreviousId = mapping.map({ $0.0.id }).max()
var sentStickers: [TelegramMediaFile] = []
var sentGifs: [TelegramMediaFile] = []
var updatedGroupingKey: [Int64 : [MessageId]] = [:]
for (message, _, updatedMessage) in mapping {
if let groupingKey = updatedMessage.groupingKey {
var ids = updatedGroupingKey[groupingKey] ?? []
ids.append(message.id)
updatedGroupingKey[groupingKey] = ids
}
}
if let latestPreviousId = latestPreviousId, let latestIndex = mapping.last?.1 {
transaction.offsetPendingMessagesTimestamps(lowerBound: latestPreviousId, excludeIds: Set(mapping.map { $0.0.id }), timestamp: latestIndex.timestamp)
}
for (key, ids) in updatedGroupingKey {
transaction.updateMessageGroupingKeysAtomically(ids, groupingKey: key)
}
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
if let (message, _, updatedMessage) = mapping.first {
for attribute in message.attributes {
if let attribute = attribute as? OutgoingQuickReplyMessageAttribute {
if let threadId = updatedMessage.threadId {
_internal_applySentQuickReplyMessage(transaction: transaction, shortcut: attribute.shortcut, quickReplyId: Int32(clamping: threadId))
}
}
}
}
for (message, _, updatedMessage) in mapping {
transaction.updateMessage(message.id, update: { currentMessage in
let updatedId: MessageId
if case let .Id(id) = updatedMessage.id {
updatedId = id
} else {
updatedId = currentMessage.id
}
for attribute in currentMessage.attributes {
if let attribute = attribute as? OutgoingMessageInfoAttribute {
for id in attribute.bubbleUpEmojiOrStickersets {
if !bubbleUpEmojiOrStickersets.contains(id) {
bubbleUpEmojiOrStickersets.append(id)
}
}
}
}
let media: [Media]
let attributes: [MessageAttribute]
let text: String
media = updatedMessage.media
attributes = updatedMessage.attributes
text = updatedMessage.text
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
if let fromMedia = currentMessage.media.first, let toMedia = media.first {
applyMediaResourceChanges(from: fromMedia, to: toMedia, postbox: postbox, force: false)
}
if storeForwardInfo == nil {
inner: for media in message.media {
if let file = media as? TelegramMediaFile {
for attribute in file.attributes {
switch attribute {
case let .Sticker(_, packReference, _):
if packReference != nil {
sentStickers.append(file)
}
case .Animated:
if !file.isAnimatedSticker && !file.isVideoSticker {
sentGifs.append(file)
}
default:
break
}
}
break inner
}
}
}
var entitiesAttribute: TextEntitiesMessageAttribute?
for attribute in attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entitiesAttribute = attribute
break
}
}
let (tags, globalTags) = tagsForStoreMessage(incoming: currentMessage.flags.contains(.Incoming), attributes: attributes, media: media, textEntities: entitiesAttribute?.entities, isPinned: currentMessage.tags.contains(.pinned))
return .update(StoreMessage(id: updatedId, customStableId: nil, globallyUniqueId: nil, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: updatedMessage.timestamp, flags: [], tags: tags, globalTags: globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: text, attributes: attributes, media: media))
})
}
for file in sentStickers {
if let entry = CodableEntry(RecentMediaItem(file)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStickers, item: OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry), removeTailIfCountExceeds: 20)
}
}
for file in sentGifs {
if !file.hasLinkedStickers {
if let entry = CodableEntry(RecentMediaItem(file)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentGifs, item: OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry), removeTailIfCountExceeds: 200)
}
}
}
if !bubbleUpEmojiOrStickersets.isEmpty {
applyBubbleUpEmojiOrStickersets(transaction: transaction, ids: bubbleUpEmojiOrStickersets)
}
stateManager.addUpdates(result)
stateManager.addUpdateGroups([.ensurePeerHasLocalState(id: messages[0].id.peerId)])
pendingMessageEvents(mapping.compactMap { message, _, updatedMessage -> PeerPendingMessageDelivered? in
guard case let .Id(id) = updatedMessage.id else {
return nil
}
return PeerPendingMessageDelivered(
id: id,
isSilent: updatedMessage.attributes.contains(where: { attribute in
if let attribute = attribute as? NotificationInfoMessageAttribute {
return attribute.flags.contains(.muted)
} else {
return false
}
}),
isPendingProcessing: updatedMessage.attributes.contains(where: { $0 is PendingProcessingMessageAttribute })
)
})
}
}
private func applyBubbleUpEmojiOrStickersets(transaction: Transaction, ids: [ItemCollectionId]) {
let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks, Namespaces.ItemCollection.CloudEmojiPacks]
for namespace in namespaces {
let namespaceIds = ids.filter { $0.namespace == namespace }
if !namespaceIds.isEmpty {
let infos = transaction.getItemCollectionsInfos(namespace: namespace)
var packDict: [ItemCollectionId: Int] = [:]
for i in 0 ..< infos.count {
packDict[infos[i].0] = i
}
var topSortedPacks: [(ItemCollectionId, ItemCollectionInfo)] = []
var processedPacks = Set<ItemCollectionId>()
for id in namespaceIds {
if let index = packDict[id] {
topSortedPacks.append(infos[index])
processedPacks.insert(id)
}
}
let restPacks = infos.filter { !processedPacks.contains($0.0) }
let sortedPacks = topSortedPacks + restPacks
transaction.replaceItemCollectionInfos(namespace: namespace, itemCollectionInfos: sortedPacks)
}
}
}
@@ -0,0 +1,317 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import FlatBuffers
import FlatSerialization
public final class AvailableMessageEffects: Equatable, Codable {
public final class MessageEffect: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case id
case isPremium
case emoticon
case staticIcon
case staticIconData = "sid"
case effectSticker
case effectStickerData = "esd"
case effectAnimation
case effectAnimationData = "ead"
}
public let id: Int64
public let isPremium: Bool
public let emoticon: String
public let staticIcon: TelegramMediaFile.Accessor?
public let effectSticker: TelegramMediaFile.Accessor
public let effectAnimation: TelegramMediaFile.Accessor?
public init(
id: Int64,
isPremium: Bool,
emoticon: String,
staticIcon: TelegramMediaFile.Accessor?,
effectSticker: TelegramMediaFile.Accessor,
effectAnimation: TelegramMediaFile.Accessor?
) {
self.id = id
self.isPremium = isPremium
self.emoticon = emoticon
self.staticIcon = staticIcon
self.effectSticker = effectSticker
self.effectAnimation = effectAnimation
}
public static func ==(lhs: MessageEffect, rhs: MessageEffect) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.isPremium != rhs.isPremium {
return false
}
if lhs.emoticon != rhs.emoticon {
return false
}
if lhs.staticIcon != rhs.staticIcon {
return false
}
if lhs.effectSticker != rhs.effectSticker {
return false
}
if lhs.effectAnimation != rhs.effectAnimation {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
self.isPremium = try container.decodeIfPresent(Bool.self, forKey: .isPremium) ?? false
self.emoticon = try container.decode(String.self, forKey: .emoticon)
if let staticIconData = try container.decodeIfPresent(Data.self, forKey: .staticIconData) {
var byteBuffer = ByteBuffer(data: staticIconData)
self.staticIcon = TelegramMediaFile.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_TelegramMediaFile, staticIconData)
} else if let staticIconData = try container.decodeIfPresent(AdaptedPostboxDecoder.RawObjectData.self, forKey: .staticIcon) {
self.staticIcon = TelegramMediaFile.Accessor(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: staticIconData.data))))
} else {
self.staticIcon = nil
}
if let effectStickerData = try container.decodeIfPresent(Data.self, forKey: .effectStickerData) {
var byteBuffer = ByteBuffer(data: effectStickerData)
self.effectSticker = TelegramMediaFile.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_TelegramMediaFile, effectStickerData)
} else {
let effectStickerData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .effectSticker)
self.effectSticker = TelegramMediaFile.Accessor(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: effectStickerData.data))))
}
if let effectAnimationData = try container.decodeIfPresent(Data.self, forKey: .effectAnimationData) {
var byteBuffer = ByteBuffer(data: effectAnimationData)
self.effectAnimation = TelegramMediaFile.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_TelegramMediaFile, effectAnimationData)
} else if let effectAnimationData = try container.decodeIfPresent(AdaptedPostboxDecoder.RawObjectData.self, forKey: .effectAnimation) {
self.effectAnimation = TelegramMediaFile.Accessor(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: effectAnimationData.data))))
} else {
self.effectAnimation = nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.emoticon, forKey: .emoticon)
try container.encode(self.isPremium, forKey: .isPremium)
let encodeFileItem: (TelegramMediaFile.Accessor, CodingKeys) throws -> Void = { file, key in
if let serializedFile = file._wrappedData {
try container.encode(serializedFile, forKey: key)
} else if let file = file._wrappedFile {
var builder = FlatBufferBuilder(initialSize: 1024)
let value = file.encodeToFlatBuffers(builder: &builder)
builder.finish(offset: value)
let serializedFile = builder.data
try container.encode(serializedFile, forKey: key)
} else {
preconditionFailure()
}
}
if let staticIcon = self.staticIcon {
try encodeFileItem(staticIcon, .staticIconData)
}
try encodeFileItem(self.effectSticker, .effectStickerData)
if let effectAnimation = self.effectAnimation {
try encodeFileItem(effectAnimation, .effectAnimationData)
}
}
}
private enum CodingKeys: String, CodingKey {
case newHash
case messageEffects
}
public let hash: Int32
public let messageEffects: [MessageEffect]
public init(
hash: Int32,
messageEffects: [MessageEffect]
) {
self.hash = hash
self.messageEffects = messageEffects
}
public static func ==(lhs: AvailableMessageEffects, rhs: AvailableMessageEffects) -> Bool {
if lhs.hash != rhs.hash {
return false
}
if lhs.messageEffects != rhs.messageEffects {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.hash = try container.decodeIfPresent(Int32.self, forKey: .newHash) ?? 0
self.messageEffects = try container.decode([MessageEffect].self, forKey: .messageEffects)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.hash, forKey: .newHash)
try container.encode(self.messageEffects, forKey: .messageEffects)
}
}
private extension AvailableMessageEffects.MessageEffect {
convenience init?(apiMessageEffect: Api.AvailableEffect, files: [Int64: TelegramMediaFile]) {
switch apiMessageEffect {
case let .availableEffect(flags, id, emoticon, staticIconId, effectStickerId, effectAnimationId):
guard let effectSticker = files[effectStickerId] else {
return nil
}
let isPremium = (flags & (1 << 2)) != 0
self.init(
id: id,
isPremium: isPremium,
emoticon: emoticon,
staticIcon: staticIconId.flatMap({ files[$0].flatMap(TelegramMediaFile.Accessor.init) }),
effectSticker: TelegramMediaFile.Accessor(effectSticker),
effectAnimation: effectAnimationId.flatMap({ files[$0].flatMap(TelegramMediaFile.Accessor.init) })
)
}
}
}
func _internal_cachedAvailableMessageEffects(postbox: Postbox) -> Signal<AvailableMessageEffects?, NoError> {
return postbox.transaction { transaction -> AvailableMessageEffects? in
return _internal_cachedAvailableMessageEffects(transaction: transaction)
}
}
func _internal_cachedAvailableMessageEffects(transaction: Transaction) -> AvailableMessageEffects? {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.availableMessageEffects, key: key))?.get(AvailableMessageEffects.self)
if let cached = cached {
return cached
} else {
return nil
}
}
func _internal_setCachedAvailableMessageEffects(transaction: Transaction, availableMessageEffects: AvailableMessageEffects) {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
if let entry = CodableEntry(availableMessageEffects) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.availableMessageEffects, key: key), entry: entry)
}
}
func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Network) -> Signal<Never, NoError> {
let poll = Signal<Never, NoError> { subscriber in
let signal: Signal<Never, NoError> = _internal_cachedAvailableMessageEffects(postbox: postbox)
|> mapToSignal { current in
let sourceHash: Int32
#if DEBUG
sourceHash = 0
#else
sourceHash = current?.hash ?? 0
#endif
return (network.request(Api.functions.messages.getAvailableEffects(hash: sourceHash))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AvailableEffects?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
return postbox.transaction { transaction -> Signal<Never, NoError> in
guard let result = result else {
return .complete()
}
switch result {
case let .availableEffects(hash, effects, documents):
var files: [Int64: TelegramMediaFile] = [:]
for document in documents {
if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) {
files[file.fileId.id] = file
}
}
var parsedEffects: [AvailableMessageEffects.MessageEffect] = []
for effect in effects {
if let parsedEffect = AvailableMessageEffects.MessageEffect(apiMessageEffect: effect, files: files) {
parsedEffects.append(parsedEffect)
}
}
_internal_setCachedAvailableMessageEffects(transaction: transaction, availableMessageEffects: AvailableMessageEffects(
hash: hash,
messageEffects: parsedEffects
))
case .availableEffectsNotModified:
break
}
/*var signals: [Signal<Never, NoError>] = []
if let availableMessageEffects = _internal_cachedAvailableMessageEffects(transaction: transaction) {
var resources: [MediaResource] = []
for messageEffect in availableMessageEffects.messageEffects {
if let staticIcon = messageEffect.staticIcon {
resources.append(staticIcon.resource)
}
if messageEffect.effectSticker.isPremiumSticker {
if let effectFile = messageEffect.effectSticker.videoThumbnails.first {
resources.append(effectFile.resource)
}
} else {
if let effectAnimation = messageEffect.effectAnimation {
resources.append(effectAnimation.resource)
}
}
}
for resource in resources {
signals.append(
fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .standalone(resource: resource))
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
)
}
}
return combineLatest(signals)
|> ignoreValues*/
return .complete()
}
|> switchToLatest
})
}
return signal.start(completed: {
subscriber.putCompletion()
})
}
return (
poll
|> then(
.complete()
|> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue())
)
)
|> restart
}
@@ -0,0 +1,482 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import FlatBuffers
import FlatSerialization
private func generateStarsReactionFile(kind: Int, isAnimatedSticker: Bool) -> TelegramMediaFile {
let baseId: Int64 = 52343278047832950 + 10
let fileId = baseId + Int64(kind)
var attributes: [TelegramMediaFileAttribute] = []
attributes.append(TelegramMediaFileAttribute.FileName(fileName: isAnimatedSticker ? "sticker.tgs" : "sticker.webp"))
if !isAnimatedSticker {
attributes.append(.CustomEmoji(isPremium: false, isSingleColor: false, alt: ".", packReference: nil))
}
return TelegramMediaFile(
fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: fileId),
partialReference: nil,
resource: LocalFileMediaResource(fileId: fileId),
previewRepresentations: [],
videoThumbnails: [],
immediateThumbnailData: nil,
mimeType: isAnimatedSticker ? "application/x-tgsticker" : "image/webp",
size: nil,
attributes: attributes,
alternativeRepresentations: []
)
}
private let sharedStarsReaction: AvailableReactions.Reaction = {
return AvailableReactions.Reaction(
isEnabled: false,
isPremium: false,
value: .stars,
title: "Star",
staticIcon: generateStarsReactionFile(kind: 0, isAnimatedSticker: true),
appearAnimation: generateStarsReactionFile(kind: 1, isAnimatedSticker: true),
selectAnimation: generateStarsReactionFile(kind: 2, isAnimatedSticker: true),
activateAnimation: generateStarsReactionFile(kind: 3, isAnimatedSticker: true),
effectAnimation: generateStarsReactionFile(kind: 4, isAnimatedSticker: true),
aroundAnimation: generateStarsReactionFile(kind: 5, isAnimatedSticker: true),
centerAnimation: generateStarsReactionFile(kind: 6, isAnimatedSticker: true)
)
}()
public final class AvailableReactions: Equatable, Codable {
public final class Reaction: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case isEnabled
case isPremium
case value
case title
case staticIcon
case staticIconData
case appearAnimation
case appearAnimationData
case selectAnimation
case selectAnimationData
case activateAnimation
case activateAnimationData
case effectAnimation
case effectAnimationData
case aroundAnimation
case aroundAnimationData
case centerAnimation
case centerAnimationData
case isStars
}
public let isEnabled: Bool
public let isPremium: Bool
public let value: MessageReaction.Reaction
public let title: String
public let staticIcon: TelegramMediaFile.Accessor
public let appearAnimation: TelegramMediaFile.Accessor
public let selectAnimation: TelegramMediaFile.Accessor
public let activateAnimation: TelegramMediaFile.Accessor
public let effectAnimation: TelegramMediaFile.Accessor
public let aroundAnimation: TelegramMediaFile.Accessor?
public let centerAnimation: TelegramMediaFile.Accessor?
public init(
isEnabled: Bool,
isPremium: Bool,
value: MessageReaction.Reaction,
title: String,
staticIcon: TelegramMediaFile,
appearAnimation: TelegramMediaFile,
selectAnimation: TelegramMediaFile,
activateAnimation: TelegramMediaFile,
effectAnimation: TelegramMediaFile,
aroundAnimation: TelegramMediaFile?,
centerAnimation: TelegramMediaFile?
) {
self.isEnabled = isEnabled
self.isPremium = isPremium
self.value = value
self.title = title
self.staticIcon = TelegramMediaFile.Accessor(staticIcon)
self.appearAnimation = TelegramMediaFile.Accessor(appearAnimation)
self.selectAnimation = TelegramMediaFile.Accessor(selectAnimation)
self.activateAnimation = TelegramMediaFile.Accessor(activateAnimation)
self.effectAnimation = TelegramMediaFile.Accessor(effectAnimation)
self.aroundAnimation = aroundAnimation.flatMap(TelegramMediaFile.Accessor.init)
self.centerAnimation = centerAnimation.flatMap(TelegramMediaFile.Accessor.init)
}
public static func ==(lhs: Reaction, rhs: Reaction) -> Bool {
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.isPremium != rhs.isPremium {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.staticIcon != rhs.staticIcon {
return false
}
if lhs.appearAnimation != rhs.appearAnimation {
return false
}
if lhs.selectAnimation != rhs.selectAnimation {
return false
}
if lhs.activateAnimation != rhs.activateAnimation {
return false
}
if lhs.effectAnimation != rhs.effectAnimation {
return false
}
if lhs.aroundAnimation != rhs.aroundAnimation {
return false
}
if lhs.centerAnimation != rhs.centerAnimation {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.isEnabled = try container.decode(Bool.self, forKey: .isEnabled)
self.isPremium = try container.decodeIfPresent(Bool.self, forKey: .isPremium) ?? false
let isStars = try container.decodeIfPresent(Bool.self, forKey: .isStars) ?? false
if isStars {
self.value = .stars
} else {
self.value = .builtin(try container.decode(String.self, forKey: .value))
}
self.title = try container.decode(String.self, forKey: .title)
if let staticIconData = try container.decodeIfPresent(Data.self, forKey: .staticIconData) {
var byteBuffer = ByteBuffer(data: staticIconData)
self.staticIcon = TelegramMediaFile.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_TelegramMediaFile, staticIconData)
} else {
let staticIconData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .staticIcon)
self.staticIcon = TelegramMediaFile.Accessor(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: staticIconData.data))))
}
if let appearAnimationData = try container.decodeIfPresent(Data.self, forKey: .appearAnimationData) {
var byteBuffer = ByteBuffer(data: appearAnimationData)
self.appearAnimation = TelegramMediaFile.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_TelegramMediaFile, appearAnimationData)
} else {
let appearAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .appearAnimation)
self.appearAnimation = TelegramMediaFile.Accessor(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: appearAnimationData.data))))
}
if let selectAnimationData = try container.decodeIfPresent(Data.self, forKey: .selectAnimationData) {
var byteBuffer = ByteBuffer(data: selectAnimationData)
self.selectAnimation = TelegramMediaFile.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_TelegramMediaFile, selectAnimationData)
} else {
let selectAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .selectAnimation)
self.selectAnimation = TelegramMediaFile.Accessor(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: selectAnimationData.data))))
}
if let activateAnimationData = try container.decodeIfPresent(Data.self, forKey: .activateAnimationData) {
var byteBuffer = ByteBuffer(data: activateAnimationData)
self.activateAnimation = TelegramMediaFile.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_TelegramMediaFile, activateAnimationData)
} else {
let activateAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .activateAnimation)
self.activateAnimation = TelegramMediaFile.Accessor(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: activateAnimationData.data))))
}
if let effectAnimationData = try container.decodeIfPresent(Data.self, forKey: .effectAnimationData) {
var byteBuffer = ByteBuffer(data: effectAnimationData)
self.effectAnimation = TelegramMediaFile.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_TelegramMediaFile, effectAnimationData)
} else {
let effectAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .effectAnimation)
self.effectAnimation = TelegramMediaFile.Accessor(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: effectAnimationData.data))))
}
if let aroundAnimationData = try container.decodeIfPresent(Data.self, forKey: .aroundAnimationData) {
var byteBuffer = ByteBuffer(data: aroundAnimationData)
self.aroundAnimation = TelegramMediaFile.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_TelegramMediaFile, aroundAnimationData)
} else if let aroundAnimationData = try container.decodeIfPresent(AdaptedPostboxDecoder.RawObjectData.self, forKey: .aroundAnimation) {
self.aroundAnimation = TelegramMediaFile.Accessor(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: aroundAnimationData.data))))
} else {
self.aroundAnimation = nil
}
if let centerAnimationData = try container.decodeIfPresent(Data.self, forKey: .centerAnimationData) {
var byteBuffer = ByteBuffer(data: centerAnimationData)
self.centerAnimation = TelegramMediaFile.Accessor(FlatBuffers_getRoot(byteBuffer: &byteBuffer) as TelegramCore_TelegramMediaFile, centerAnimationData)
} else if let centerAnimationData = try container.decodeIfPresent(AdaptedPostboxDecoder.RawObjectData.self, forKey: .centerAnimation) {
self.centerAnimation = TelegramMediaFile.Accessor(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: centerAnimationData.data))))
} else {
self.centerAnimation = nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.isEnabled, forKey: .isEnabled)
try container.encode(self.isPremium, forKey: .isPremium)
switch self.value {
case let .builtin(value):
try container.encode(value, forKey: .value)
case .custom:
break
case .stars:
try container.encode(true, forKey: .isStars)
}
try container.encode(self.title, forKey: .title)
let encodeFileItem: (TelegramMediaFile.Accessor, CodingKeys) throws -> Void = { file, key in
if let serializedFile = file._wrappedData {
try container.encode(serializedFile, forKey: key)
} else if let file = file._wrappedFile {
var builder = FlatBufferBuilder(initialSize: 1024)
let value = file.encodeToFlatBuffers(builder: &builder)
builder.finish(offset: value)
let serializedFile = builder.data
try container.encode(serializedFile, forKey: key)
} else {
preconditionFailure()
}
}
try encodeFileItem(self.staticIcon, .staticIconData)
try encodeFileItem(self.appearAnimation, .appearAnimationData)
try encodeFileItem(self.selectAnimation, .selectAnimationData)
try encodeFileItem(self.activateAnimation, .activateAnimationData)
try encodeFileItem(self.effectAnimation, .effectAnimationData)
if let aroundAnimation = self.aroundAnimation {
try encodeFileItem(aroundAnimation, .aroundAnimationData)
}
if let centerAnimation = self.centerAnimation {
try encodeFileItem(centerAnimation, .centerAnimationData)
}
}
}
private enum CodingKeys: String, CodingKey {
case newHash
case reactions
}
public let hash: Int32
public let reactions: [Reaction]
public init(
hash: Int32,
reactions: [Reaction]
) {
self.hash = hash
var reactions = reactions
reactions.removeAll(where: { if case .stars = $0.value { return true } else { return false } })
reactions.append(sharedStarsReaction)
self.reactions = reactions
}
public static func ==(lhs: AvailableReactions, rhs: AvailableReactions) -> Bool {
if lhs.hash != rhs.hash {
return false
}
if lhs.reactions != rhs.reactions {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.hash = try container.decodeIfPresent(Int32.self, forKey: .newHash) ?? 0
var reactions = try container.decode([Reaction].self, forKey: .reactions)
reactions.removeAll(where: { if case .stars = $0.value { return true } else { return false } })
reactions.append(sharedStarsReaction)
self.reactions = reactions
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.hash, forKey: .newHash)
try container.encode(self.reactions, forKey: .reactions)
}
}
private extension AvailableReactions.Reaction {
convenience init?(apiReaction: Api.AvailableReaction) {
switch apiReaction {
case let .availableReaction(flags, reaction, title, staticIcon, appearAnimation, selectAnimation, activateAnimation, effectAnimation, aroundAnimation, centerIcon):
guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon, altDocuments: []) else {
return nil
}
guard let appearAnimationFile = telegramMediaFileFromApiDocument(appearAnimation, altDocuments: []) else {
return nil
}
guard let selectAnimationFile = telegramMediaFileFromApiDocument(selectAnimation, altDocuments: []) else {
return nil
}
guard let activateAnimationFile = telegramMediaFileFromApiDocument(activateAnimation, altDocuments: []) else {
return nil
}
guard let effectAnimationFile = telegramMediaFileFromApiDocument(effectAnimation, altDocuments: []) else {
return nil
}
let aroundAnimationFile = aroundAnimation.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }
let centerAnimationFile = centerIcon.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }
let isEnabled = (flags & (1 << 0)) == 0
let isPremium = (flags & (1 << 2)) != 0
self.init(
isEnabled: isEnabled,
isPremium: isPremium,
value: .builtin(reaction),
title: title,
staticIcon: staticIconFile,
appearAnimation: appearAnimationFile,
selectAnimation: selectAnimationFile,
activateAnimation: activateAnimationFile,
effectAnimation: effectAnimationFile,
aroundAnimation: aroundAnimationFile,
centerAnimation: centerAnimationFile
)
}
}
}
func _internal_cachedAvailableReactions(postbox: Postbox) -> Signal<AvailableReactions?, NoError> {
return postbox.transaction { transaction -> AvailableReactions? in
return _internal_cachedAvailableReactions(transaction: transaction)
}
}
func _internal_cachedAvailableReactions(transaction: Transaction) -> AvailableReactions? {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.availableReactions, key: key))?.get(AvailableReactions.self)
if let cached = cached {
return cached
} else {
return nil
}
}
func _internal_setCachedAvailableReactions(transaction: Transaction, availableReactions: AvailableReactions) {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
if let entry = CodableEntry(availableReactions) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.availableReactions, key: key), entry: entry)
}
}
func managedSynchronizeAvailableReactions(postbox: Postbox, network: Network) -> Signal<Never, NoError> {
let starsReaction = sharedStarsReaction
let mapping: [String: KeyPath<AvailableReactions.Reaction, TelegramMediaFile.Accessor>] = [
"star_reaction_activate.tgs": \.activateAnimation,
"star_reaction_appear.tgs": \.appearAnimation,
"star_reaction_effect.tgs": \.effectAnimation,
"star_reaction_select.tgs": \.selectAnimation,
"star_reaction_static_icon.webp": \.staticIcon
]
let optionalMapping: [String: KeyPath<AvailableReactions.Reaction, TelegramMediaFile.Accessor?>] = [
"star_reaction_center.tgs": \.centerAnimation,
"star_reaction_effect.tgs": \.aroundAnimation
]
for (key, path) in mapping {
if let filePath = Bundle.main.path(forResource: key, ofType: nil), let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) {
postbox.mediaBox.storeResourceData(starsReaction[keyPath: path]._parse().resource.id, data: data)
}
}
for (key, path) in optionalMapping {
if let filePath = Bundle.main.path(forResource: key, ofType: nil), let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) {
if let file = starsReaction[keyPath: path] {
postbox.mediaBox.storeResourceData(file._parse().resource.id, data: data)
}
}
}
let poll = Signal<Never, NoError> { subscriber in
let signal: Signal<Never, NoError> = _internal_cachedAvailableReactions(postbox: postbox)
|> mapToSignal { current in
return (network.request(Api.functions.messages.getAvailableReactions(hash: current?.hash ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AvailableReactions?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
return postbox.transaction { transaction -> Signal<Never, NoError> in
guard let result = result else {
return .complete()
}
switch result {
case let .availableReactions(hash, reactions):
let availableReactions = AvailableReactions(
hash: hash,
reactions: reactions.compactMap(AvailableReactions.Reaction.init(apiReaction:))
)
_internal_setCachedAvailableReactions(transaction: transaction, availableReactions: availableReactions)
case .availableReactionsNotModified:
break
}
var signals: [Signal<Never, NoError>] = []
if let availableReactions = _internal_cachedAvailableReactions(transaction: transaction) {
var resources: [MediaResource] = []
for reaction in availableReactions.reactions {
resources.append(reaction.staticIcon._parse().resource)
resources.append(reaction.appearAnimation._parse().resource)
resources.append(reaction.selectAnimation._parse().resource)
resources.append(reaction.activateAnimation._parse().resource)
resources.append(reaction.effectAnimation._parse().resource)
if let centerAnimation = reaction.centerAnimation {
resources.append(centerAnimation._parse().resource)
}
if let aroundAnimation = reaction.aroundAnimation {
resources.append(aroundAnimation._parse().resource)
}
}
for resource in resources {
signals.append(
fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .standalone(resource: resource))
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
)
}
}
return combineLatest(signals)
|> ignoreValues
}
|> switchToLatest
})
}
return signal.start(completed: {
subscriber.putCompletion()
})
}
return (
poll
|> then(
.complete()
|> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue())
)
)
|> restart
}
@@ -0,0 +1,54 @@
import Foundation
import Postbox
import SwiftSignalKit
enum CachedSentMediaReferenceKey {
case image(hash: Data)
case file(hash: Data)
var key: ValueBoxKey {
switch self {
case let .image(hash):
let result = ValueBoxKey(length: 1 + hash.count)
result.setUInt8(0, value: 0)
hash.withUnsafeBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: Int8.self)
memcpy(result.memory.advanced(by: 1), bytes, hash.count)
}
return result
case let .file(hash):
let result = ValueBoxKey(length: 1 + hash.count)
result.setUInt8(0, value: 1)
hash.withUnsafeBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: Int8.self)
memcpy(result.memory.advanced(by: 1), bytes, hash.count)
}
return result
}
}
}
private struct CachedMediaReferenceEntry: Codable {
var data: Data
}
func cachedSentMediaReference(postbox: Postbox, key: CachedSentMediaReferenceKey) -> Signal<Media?, NoError> {
return postbox.transaction { transaction -> Media? in
guard let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedSentMediaReferences, key: key.key))?.get(CachedMediaReferenceEntry.self) else {
return nil
}
return PostboxDecoder(buffer: MemoryBuffer(data: entry.data)).decodeRootObject() as? Media
}
}
func storeCachedSentMediaReference(transaction: Transaction, key: CachedSentMediaReferenceKey, media: Media) {
let encoder = PostboxEncoder()
encoder.encodeRootObject(media)
let mediaData = encoder.makeData()
guard let entry = CodableEntry(CachedMediaReferenceEntry(data: mediaData)) else {
return
}
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedSentMediaReferences, key: key.key), entry: entry)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,565 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
public struct MyBoostStatus: Equatable {
public struct Boost: Equatable {
public let slot: Int32
public let peer: EnginePeer?
public let date: Int32
public let expires: Int32
public let cooldownUntil: Int32?
public init(slot: Int32, peer: EnginePeer?, date: Int32, expires: Int32, cooldownUntil: Int32?) {
self.slot = slot
self.peer = peer
self.date = date
self.expires = expires
self.cooldownUntil = cooldownUntil
}
}
public let boosts: [Boost]
}
public struct ChannelBoostStatus: Equatable {
public let level: Int
public let boosts: Int
public let giftBoosts: Int?
public let currentLevelBoosts: Int
public let nextLevelBoosts: Int?
public let premiumAudience: StatsPercentValue?
public let url: String
public let prepaidGiveaways: [PrepaidGiveaway]
public let boostedByMe: Bool
public init(level: Int, boosts: Int, giftBoosts: Int?, currentLevelBoosts: Int, nextLevelBoosts: Int?, premiumAudience: StatsPercentValue?, url: String, prepaidGiveaways: [PrepaidGiveaway], boostedByMe: Bool) {
self.level = level
self.boosts = boosts
self.giftBoosts = giftBoosts
self.currentLevelBoosts = currentLevelBoosts
self.nextLevelBoosts = nextLevelBoosts
self.premiumAudience = premiumAudience
self.url = url
self.prepaidGiveaways = prepaidGiveaways
self.boostedByMe = boostedByMe
}
public static func ==(lhs: ChannelBoostStatus, rhs: ChannelBoostStatus) -> Bool {
if lhs.level != rhs.level {
return false
}
if lhs.boosts != rhs.boosts {
return false
}
if lhs.giftBoosts != rhs.giftBoosts {
return false
}
if lhs.currentLevelBoosts != rhs.currentLevelBoosts {
return false
}
if lhs.nextLevelBoosts != rhs.nextLevelBoosts {
return false
}
if lhs.premiumAudience != rhs.premiumAudience {
return false
}
if lhs.url != rhs.url {
return false
}
if lhs.prepaidGiveaways != rhs.prepaidGiveaways {
return false
}
if lhs.boostedByMe != rhs.boostedByMe {
return false
}
return true
}
public func withUpdated(boosts: Int) -> ChannelBoostStatus {
return ChannelBoostStatus(level: self.level, boosts: boosts, giftBoosts: self.giftBoosts, currentLevelBoosts: self.currentLevelBoosts, nextLevelBoosts: self.nextLevelBoosts, premiumAudience: self.premiumAudience, url: self.url, prepaidGiveaways: self.prepaidGiveaways, boostedByMe: self.boostedByMe)
}
}
func _internal_getChannelBoostStatus(account: Account, peerId: PeerId) -> Signal<ChannelBoostStatus?, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<ChannelBoostStatus?, NoError> in
guard let inputPeer = inputPeer else {
return .single(nil)
}
return account.network.request(Api.functions.premium.getBoostsStatus(peer: inputPeer))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.premium.BoostsStatus?, NoError> in
return .single(nil)
}
|> map { result -> ChannelBoostStatus? in
guard let result = result else {
return nil
}
switch result {
case let .boostsStatus(flags, level, currentLevelBoosts, boosts, giftBoosts, nextLevelBoosts, premiumAudience, boostUrl, prepaidGiveaways, myBoostSlots):
let _ = myBoostSlots
return ChannelBoostStatus(level: Int(level), boosts: Int(boosts), giftBoosts: giftBoosts.flatMap(Int.init), currentLevelBoosts: Int(currentLevelBoosts), nextLevelBoosts: nextLevelBoosts.flatMap(Int.init), premiumAudience: premiumAudience.flatMap({ StatsPercentValue(apiPercentValue: $0) }), url: boostUrl, prepaidGiveaways: prepaidGiveaways?.map({ PrepaidGiveaway(apiPrepaidGiveaway: $0) }) ?? [], boostedByMe: (flags & (1 << 2)) != 0)
}
}
}
}
func _internal_applyChannelBoost(account: Account, peerId: PeerId, slots: [Int32]) -> Signal<MyBoostStatus?, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<MyBoostStatus?, NoError> in
guard let inputPeer = inputPeer else {
return .complete()
}
var flags: Int32 = 0
if !slots.isEmpty {
flags |= (1 << 0)
}
return account.network.request(Api.functions.premium.applyBoost(flags: flags, slots: !slots.isEmpty ? slots : nil, peer: inputPeer))
|> map (Optional.init)
|> `catch` { error -> Signal<Api.premium.MyBoosts?, NoError> in
return .complete()
}
|> mapToSignal { result -> Signal<MyBoostStatus?, NoError> in
if let result = result {
return account.postbox.transaction { transaction -> MyBoostStatus? in
let myStatus = MyBoostStatus(apiMyBoostStatus: result, accountPeerId: account.peerId, transaction: transaction)
let peerIds = myStatus.boosts.reduce(Set<PeerId>(), { current, value in
var current = current
if let peerId = value.peer?.id {
current.insert(peerId)
}
return current
})
transaction.updatePeerCachedData(peerIds: peerIds, update: { peerId, cachedData in
let cachedData = cachedData as? CachedChannelData ?? CachedChannelData()
let count = myStatus.boosts.filter { $0.peer?.id == peerId }.count
return cachedData.withUpdatedAppliedBoosts(count != 0 ? Int32(count) : nil)
})
return myStatus
}
} else {
return .single(nil)
}
}
}
}
func _internal_getMyBoostStatus(account: Account) -> Signal<MyBoostStatus?, NoError> {
return account.network.request(Api.functions.premium.getMyBoosts())
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.premium.MyBoosts?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<MyBoostStatus?, NoError> in
guard let result = result else {
return .single(nil)
}
return account.postbox.transaction { transaction -> MyBoostStatus? in
return MyBoostStatus(apiMyBoostStatus: result, accountPeerId: account.peerId, transaction: transaction)
}
}
}
private final class ChannelBoostersContextImpl {
private let queue: Queue
private let account: Account
private let peerId: PeerId
private let gift: Bool
private let disposable = MetaDisposable()
private let updateDisposables = DisposableSet()
private var isLoadingMore: Bool = false
private var hasLoadedOnce: Bool = false
private var canLoadMore: Bool = true
private var loadedFromCache = false
private var results: [ChannelBoostersContext.State.Boost] = []
private var count: Int32
private var lastOffset: String?
private var populateCache: Bool = true
let state = Promise<ChannelBoostersContext.State>()
init(queue: Queue, account: Account, peerId: PeerId, gift: Bool) {
self.queue = queue
self.account = account
self.peerId = peerId
self.gift = gift
self.count = 0
self.isLoadingMore = true
self.disposable.set((account.postbox.transaction { transaction -> (peers: [ChannelBoostersContext.State.Boost], count: Int32, canLoadMore: Bool)? in
let cachedResult = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosts, key: CachedChannelBoosters.key(peerId: peerId)))?.get(CachedChannelBoosters.self)
if let cachedResult = cachedResult, !gift {
var result: [ChannelBoostersContext.State.Boost] = []
for boost in cachedResult.boosts {
let peer = boost.peerId.flatMap { transaction.getPeer($0) }
result.append(ChannelBoostersContext.State.Boost(flags: ChannelBoostersContext.State.Boost.Flags(rawValue: boost.flags), id: boost.id, peer: peer.flatMap { EnginePeer($0) }, date: boost.date, expires: boost.expires, multiplier: boost.multiplier, stars: boost.stars, slug: boost.slug, giveawayMessageId: boost.giveawayMessageId))
}
return (result, cachedResult.count, true)
} else {
return nil
}
}
|> deliverOn(self.queue)).start(next: { [weak self] cachedPeersCountAndCanLoadMore in
guard let strongSelf = self else {
return
}
strongSelf.isLoadingMore = false
if let (cachedPeers, cachedCount, canLoadMore) = cachedPeersCountAndCanLoadMore {
strongSelf.results = cachedPeers
strongSelf.count = cachedCount
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = canLoadMore
strongSelf.loadedFromCache = true
}
strongSelf.loadMore()
}))
self.loadMore()
}
deinit {
self.disposable.dispose()
}
func reload() {
self.loadedFromCache = true
self.populateCache = true
self.loadMore()
}
func loadMore() {
if self.isLoadingMore || !self.canLoadMore {
return
}
self.isLoadingMore = true
let account = self.account
let accountPeerId = account.peerId
let peerId = self.peerId
let gift = self.gift
let populateCache = self.populateCache
if self.loadedFromCache {
self.loadedFromCache = false
}
let lastOffset = self.lastOffset
self.disposable.set((self.account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<([ChannelBoostersContext.State.Boost], Int32, String?), NoError> in
if let inputPeer = inputPeer {
let offset = lastOffset ?? ""
let limit: Int32 = lastOffset == nil ? 25 : 50
var flags: Int32 = 0
if gift {
flags |= (1 << 0)
}
let signal = account.network.request(Api.functions.premium.getBoostsList(flags: flags, peer: inputPeer, offset: offset, limit: limit))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.premium.BoostsList?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<([ChannelBoostersContext.State.Boost], Int32, String?), NoError> in
return account.postbox.transaction { transaction -> ([ChannelBoostersContext.State.Boost], Int32, String?) in
guard let result = result else {
return ([], 0, nil)
}
switch result {
case let .boostsList(_, count, boosts, nextOffset, users):
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
var resultBoosts: [ChannelBoostersContext.State.Boost] = []
for boost in boosts {
switch boost {
case let .boost(flags, id, userId, giveawayMessageId, date, expires, usedGiftSlug, multiplier, stars):
var boostFlags: ChannelBoostersContext.State.Boost.Flags = []
var boostPeer: EnginePeer?
if let userId = userId {
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
if let peer = transaction.getPeer(peerId) {
boostPeer = EnginePeer(peer)
}
}
if (flags & (1 << 1)) != 0 {
boostFlags.insert(.isGift)
}
if (flags & (1 << 2)) != 0 {
boostFlags.insert(.isGiveaway)
}
if (flags & (1 << 3)) != 0 {
boostFlags.insert(.isUnclaimed)
}
resultBoosts.append(ChannelBoostersContext.State.Boost(flags: boostFlags, id: id, peer: boostPeer, date: date, expires: expires, multiplier: multiplier ?? 1, stars: stars, slug: usedGiftSlug, giveawayMessageId: giveawayMessageId.flatMap { EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }))
}
}
if populateCache {
if let entry = CodableEntry(CachedChannelBoosters(channelPeerId: peerId, boosts: resultBoosts, count: count)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosts, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry)
}
}
return (resultBoosts, count, nextOffset)
}
}
}
return signal
} else {
return .single(([], 0, nil))
}
}
|> deliverOn(self.queue)).start(next: { [weak self] boosters, updatedCount, nextOffset in
guard let strongSelf = self else {
return
}
strongSelf.lastOffset = nextOffset
if strongSelf.populateCache {
strongSelf.populateCache = false
strongSelf.results.removeAll()
}
for booster in boosters {
strongSelf.results.append(booster)
}
strongSelf.isLoadingMore = false
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = !boosters.isEmpty && nextOffset != nil
if strongSelf.canLoadMore {
var resultsCount: Int32 = 0
for result in strongSelf.results {
resultsCount += result.multiplier
}
strongSelf.count = max(updatedCount, resultsCount)
} else {
var resultsCount: Int32 = 0
for result in strongSelf.results {
resultsCount += result.multiplier
}
strongSelf.count = resultsCount
}
strongSelf.updateState()
}))
self.updateState()
}
private func updateCache() {
guard self.hasLoadedOnce && !self.isLoadingMore else {
return
}
let peerId = self.peerId
let resultBoosts = Array(self.results.prefix(50))
let count = self.count
self.updateDisposables.add(self.account.postbox.transaction({ transaction in
if let entry = CodableEntry(CachedChannelBoosters(channelPeerId: peerId, boosts: resultBoosts, count: count)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedChannelBoosts, key: CachedChannelBoosters.key(peerId: peerId)), entry: entry)
}
}).start())
}
private func updateState() {
self.state.set(.single(ChannelBoostersContext.State(boosts: self.results, isLoadingMore: self.isLoadingMore, hasLoadedOnce: self.hasLoadedOnce, canLoadMore: self.canLoadMore, count: self.count)))
}
}
public final class ChannelBoostersContext {
public struct State: Equatable {
public struct Boost: Equatable {
public struct Flags: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
public static let isGift = Flags(rawValue: 1 << 0)
public static let isGiveaway = Flags(rawValue: 1 << 1)
public static let isUnclaimed = Flags(rawValue: 1 << 2)
}
public var flags: Flags
public var id: String
public var peer: EnginePeer?
public var date: Int32
public var expires: Int32
public var multiplier: Int32
public var stars: Int64?
public var slug: String?
public var giveawayMessageId: EngineMessage.Id?
}
public var boosts: [Boost]
public var isLoadingMore: Bool
public var hasLoadedOnce: Bool
public var canLoadMore: Bool
public var count: Int32
public static var Empty = State(boosts: [], isLoadingMore: false, hasLoadedOnce: true, canLoadMore: false, count: 0)
public static var Loading = State(boosts: [], isLoadingMore: false, hasLoadedOnce: false, canLoadMore: false, count: 0)
}
private let queue: Queue = Queue()
private let impl: QueueLocalObject<ChannelBoostersContextImpl>
public var state: Signal<State, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.state.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public init(account: Account, peerId: PeerId, gift: Bool) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return ChannelBoostersContextImpl(queue: queue, account: account, peerId: peerId, gift: gift)
})
}
public func loadMore() {
self.impl.with { impl in
impl.loadMore()
}
}
public func reload() {
self.impl.with { impl in
impl.reload()
}
}
}
private final class CachedChannelBoosters: Codable {
private enum CodingKeys: String, CodingKey {
case boosts
case count
}
fileprivate struct CachedBoost: Codable, Hashable {
private enum CodingKeys: String, CodingKey {
case flags
case id
case peerId
case date
case expires
case multiplier
case stars
case slug
case channelPeerId
case giveawayMessageId
}
var flags: Int32
var id: String
var peerId: EnginePeer.Id?
var date: Int32
var expires: Int32
var multiplier: Int32
var stars: Int64?
var slug: String?
var channelPeerId: EnginePeer.Id
var giveawayMessageId: EngineMessage.Id?
init(flags: Int32, id: String, peerId: EnginePeer.Id?, date: Int32, expires: Int32, multiplier: Int32, stars: Int64?, slug: String?, channelPeerId: EnginePeer.Id, giveawayMessageId: EngineMessage.Id?) {
self.flags = flags
self.id = id
self.peerId = peerId
self.date = date
self.expires = expires
self.multiplier = multiplier
self.stars = stars
self.slug = slug
self.channelPeerId = channelPeerId
self.giveawayMessageId = giveawayMessageId
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.flags = try container.decode(Int32.self, forKey: .flags)
self.id = try container.decode(String.self, forKey: .id)
self.peerId = try container.decodeIfPresent(Int64.self, forKey: .peerId).flatMap { EnginePeer.Id($0) }
self.date = try container.decode(Int32.self, forKey: .date)
self.expires = try container.decode(Int32.self, forKey: .expires)
self.multiplier = try container.decode(Int32.self, forKey: .multiplier)
self.stars = try container.decodeIfPresent(Int64.self, forKey: .stars)
self.slug = try container.decodeIfPresent(String.self, forKey: .slug)
self.channelPeerId = EnginePeer.Id(try container.decode(Int64.self, forKey: .channelPeerId))
self.giveawayMessageId = try container.decodeIfPresent(Int32.self, forKey: .giveawayMessageId).flatMap { EngineMessage.Id(peerId: self.channelPeerId, namespace: Namespaces.Message.Cloud, id: $0) }
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.flags, forKey: .flags)
try container.encode(self.id, forKey: .id)
try container.encodeIfPresent(self.peerId?.toInt64(), forKey: .peerId)
try container.encode(self.date, forKey: .date)
try container.encode(self.expires, forKey: .expires)
try container.encode(self.multiplier, forKey: .multiplier)
try container.encodeIfPresent(self.stars, forKey: .stars)
try container.encodeIfPresent(self.slug, forKey: .slug)
try container.encode(self.channelPeerId.toInt64(), forKey: .channelPeerId)
try container.encodeIfPresent(self.giveawayMessageId?.id, forKey: .giveawayMessageId)
}
}
fileprivate let boosts: [CachedBoost]
fileprivate let count: Int32
static func key(peerId: EnginePeer.Id) -> ValueBoxKey {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: peerId.toInt64())
return key
}
init(channelPeerId: EnginePeer.Id, boosts: [ChannelBoostersContext.State.Boost], count: Int32) {
self.boosts = boosts.map { CachedBoost(flags: $0.flags.rawValue, id: $0.id, peerId: $0.peer?.id, date: $0.date, expires: $0.expires, multiplier: $0.multiplier, stars: $0.stars, slug: $0.slug, channelPeerId: channelPeerId, giveawayMessageId: $0.giveawayMessageId) }
self.count = count
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.boosts = (try container.decode([CachedBoost].self, forKey: .boosts))
self.count = try container.decode(Int32.self, forKey: .count)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.boosts, forKey: .boosts)
try container.encode(self.count, forKey: .count)
}
}
extension MyBoostStatus {
init(apiMyBoostStatus: Api.premium.MyBoosts, accountPeerId: PeerId, transaction: Transaction) {
var boostsResult: [MyBoostStatus.Boost] = []
switch apiMyBoostStatus {
case let .myBoosts(myBoosts, chats, users):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
for boost in myBoosts {
switch boost {
case let .myBoost(_, slot, peer, date, expires, cooldownUntilDate):
var boostPeer: EnginePeer?
if let peerId = peer?.peerId, let peer = transaction.getPeer(peerId) {
boostPeer = EnginePeer(peer)
}
boostsResult.append(MyBoostStatus.Boost(slot: slot, peer: boostPeer, date: date, expires: expires, cooldownUntil: cooldownUntilDate))
}
}
}
self.boosts = boostsResult
}
}
@@ -0,0 +1,39 @@
import Foundation
import Postbox
import TelegramApi
struct ChannelUpdate {
let update: Api.Update
let ptsRange: (Int32, Int32)?
}
func channelUpdatesByPeerId(updates: [ChannelUpdate]) -> [PeerId: [ChannelUpdate]] {
var grouped: [PeerId: [ChannelUpdate]] = [:]
for update in updates {
var peerId: PeerId?
switch update.update {
case let .updateNewChannelMessage(message, _, _):
peerId = apiMessagePeerId(message)
case let .updateDeleteChannelMessages(channelId, _, _, _):
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))
case let .updateEditChannelMessage(message, _, _):
peerId = apiMessagePeerId(message)
case let .updateChannelWebPage(channelId, _, _, _):
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))
default:
break
}
if let peerId = peerId {
if grouped[peerId] == nil {
grouped[peerId] = [update]
} else {
grouped[peerId]!.append(update)
}
}
}
return grouped
}
@@ -0,0 +1,546 @@
import Foundation
import Postbox
import SwiftSignalKit
public struct HistoryPreloadIndex: Hashable, Comparable, CustomStringConvertible {
public let index: ChatListIndex?
public let threadId: Int64?
public let hasUnread: Bool
public let isMuted: Bool
public let isPriority: Bool
public init(index: ChatListIndex?, threadId: Int64?, hasUnread: Bool, isMuted: Bool, isPriority: Bool) {
self.index = index
self.threadId = threadId
self.hasUnread = hasUnread
self.isMuted = isMuted
self.isPriority = isPriority
}
public static func <(lhs: HistoryPreloadIndex, rhs: HistoryPreloadIndex) -> Bool {
if lhs.isPriority != rhs.isPriority {
if lhs.isPriority {
return true
} else {
return false
}
}
if lhs.isMuted != rhs.isMuted {
if lhs.isMuted {
return false
} else {
return true
}
}
if lhs.hasUnread != rhs.hasUnread {
if lhs.hasUnread {
return true
} else {
return false
}
}
if lhs.index == rhs.index {
if let lhsThreadId = lhs.threadId, let rhsThreadId = rhs.threadId {
if lhsThreadId != rhsThreadId {
return lhsThreadId < rhsThreadId
}
}
}
if let lhsIndex = lhs.index, let rhsIndex = rhs.index {
return lhsIndex > rhsIndex
} else if lhs.index != nil {
return true
} else if rhs.index != nil {
return false
} else {
return true
}
}
public var description: String {
return "index: \(String(describing: self.index)), hasUnread: \(self.hasUnread), isMuted: \(self.isMuted), isPriority: \(self.isPriority)"
}
}
private struct HistoryPreloadHole: Hashable, Comparable, CustomStringConvertible {
let preloadIndex: HistoryPreloadIndex
let hole: MessageOfInterestHole
static func <(lhs: HistoryPreloadHole, rhs: HistoryPreloadHole) -> Bool {
return lhs.preloadIndex < rhs.preloadIndex
}
var description: String {
return "(preloadIndex: \(self.preloadIndex), hole: \(self.hole))"
}
}
private final class HistoryPreloadEntry: Comparable {
var hole: HistoryPreloadHole
private var isStarted = false
private let disposable = MetaDisposable()
init(hole: HistoryPreloadHole) {
self.hole = hole
}
static func ==(lhs: HistoryPreloadEntry, rhs: HistoryPreloadEntry) -> Bool {
return lhs.hole == rhs.hole
}
static func <(lhs: HistoryPreloadEntry, rhs: HistoryPreloadEntry) -> Bool {
return lhs.hole < rhs.hole
}
func startIfNeeded(postbox: Postbox, accountPeerId: PeerId, download: Signal<Download, NoError>, queue: Queue) {
if !self.isStarted {
self.isStarted = true
let hole = self.hole.hole
Logger.shared.log("HistoryPreload", "start hole \(hole)")
let signal: Signal<Never, NoError> = .complete()
|> delay(0.3, queue: queue)
|> then(
download
|> take(1)
|> deliverOn(queue)
|> mapToSignal { download -> Signal<Never, NoError> in
switch hole.hole {
case let .peer(peerHole):
return fetchMessageHistoryHole(accountPeerId: accountPeerId, source: .download(download), postbox: postbox, peerInput: .direct(peerId: peerHole.peerId, threadId: nil), namespace: peerHole.namespace, direction: hole.direction, space: .everywhere, count: 60)
|> ignoreValues
}
}
)
self.disposable.set(signal.start())
}
}
deinit {
self.disposable.dispose()
}
}
private final class HistoryPreloadViewContext {
var index: ChatListIndex?
var threadId: Int64?
var hasUnread: Bool?
var isMuted: Bool?
var isPriority: Bool
let disposable = MetaDisposable()
var hole: MessageOfInterestHole?
var media: [HolesViewMedia] = []
var preloadIndex: HistoryPreloadIndex {
return HistoryPreloadIndex(index: self.index, threadId: self.threadId, hasUnread: self.hasUnread ?? false, isMuted: self.isMuted ?? true, isPriority: self.isPriority)
}
var currentHole: HistoryPreloadHole? {
if let hole = self.hole {
return HistoryPreloadHole(preloadIndex: self.preloadIndex, hole: hole)
} else {
return nil
}
}
init(index: ChatListIndex?, threadId: Int64?, hasUnread: Bool?, isMuted: Bool?, isPriority: Bool) {
self.index = index
self.threadId = threadId
self.hasUnread = hasUnread
self.isMuted = isMuted
self.isPriority = isPriority
}
deinit {
disposable.dispose()
}
}
private enum ChatHistoryPreloadEntity: Hashable {
case peer(peerId: PeerId, threadId: Int64?)
}
private struct ChatHistoryPreloadIndex {
let index: ChatListIndex
let entity: ChatHistoryPreloadEntity
}
public final class ChatHistoryPreloadMediaItem: Comparable {
public let preloadIndex: HistoryPreloadIndex
public let media: HolesViewMedia
init(preloadIndex: HistoryPreloadIndex, media: HolesViewMedia) {
self.preloadIndex = preloadIndex
self.media = media
}
public static func ==(lhs: ChatHistoryPreloadMediaItem, rhs: ChatHistoryPreloadMediaItem) -> Bool {
if lhs.preloadIndex != rhs.preloadIndex {
return false
}
if lhs.media != rhs.media {
return false
}
return true
}
public static func <(lhs: ChatHistoryPreloadMediaItem, rhs: ChatHistoryPreloadMediaItem) -> Bool {
if lhs.preloadIndex != rhs.preloadIndex {
return lhs.preloadIndex > rhs.preloadIndex
}
return lhs.media.index < rhs.media.index
}
}
private final class AdditionalPreloadPeerIdsContext {
private let queue: Queue
private var subscribers: [PeerId: Bag<Void>] = [:]
private var additionalPeerIdsValue = ValuePromise<Set<PeerId>>(Set(), ignoreRepeated: true)
var additionalPeerIds: Signal<Set<PeerId>, NoError> {
return self.additionalPeerIdsValue.get()
}
init(queue: Queue) {
self.queue = queue
}
deinit {
assert(self.queue.isCurrent())
}
func add(peerId: PeerId) -> Disposable {
let bag: Bag<Void>
if let current = self.subscribers[peerId] {
bag = current
} else {
bag = Bag()
self.subscribers[peerId] = bag
}
let wasEmpty = bag.isEmpty
let index = bag.add(Void())
if wasEmpty {
self.additionalPeerIdsValue.set(Set(self.subscribers.keys))
}
let queue = self.queue
return ActionDisposable { [weak self, weak bag] in
queue.async {
guard let strongSelf = self else {
return
}
if let current = strongSelf.subscribers[peerId], let bag = bag, current === bag {
current.remove(index)
if current.isEmpty {
strongSelf.subscribers.removeValue(forKey: peerId)
strongSelf.additionalPeerIdsValue.set(Set(strongSelf.subscribers.keys))
}
}
}
}
}
}
public struct ChatHistoryPreloadItem : Equatable, Hashable {
public let index: ChatListIndex
public let threadId: Int64?
public let isMuted: Bool
public let hasUnread: Bool
public func hash(into hasher: inout Hasher) {
hasher.combine(index.hashValue)
if let threadId = threadId {
hasher.combine(threadId)
}
}
public init(index: ChatListIndex, threadId: Int64?, isMuted: Bool, hasUnread: Bool) {
self.index = index
self.threadId = threadId
self.isMuted = isMuted
self.hasUnread = hasUnread
}
}
final class ChatHistoryPreloadManager {
private let queue = Queue()
private let postbox: Postbox
private let accountPeerId: PeerId
private let network: Network
private let download = Promise<Download>()
private var canPreloadHistoryDisposable: Disposable?
private var canPreloadHistoryValue = false
private let automaticChatListDisposable = MetaDisposable()
private var views: [ChatHistoryPreloadEntity: HistoryPreloadViewContext] = [:]
private var entries: [HistoryPreloadEntry] = []
private var orderedMediaValue: [ChatHistoryPreloadMediaItem] = []
private let orderedMediaPromise = ValuePromise<[ChatHistoryPreloadMediaItem]>([])
var orderedMedia: Signal<[ChatHistoryPreloadMediaItem], NoError> {
return self.orderedMediaPromise.get()
}
private let additionalPreloadPeerIdsContext: QueueLocalObject<AdditionalPreloadPeerIdsContext>
private let preloadItemsSignal: Signal<[ChatHistoryPreloadItem], NoError>
init(postbox: Postbox, network: Network, accountPeerId: PeerId, networkState: Signal<AccountNetworkState, NoError>, preloadItemsSignal: Signal<[ChatHistoryPreloadItem], NoError>) {
self.postbox = postbox
self.network = network
self.accountPeerId = accountPeerId
self.download.set(network.background())
self.preloadItemsSignal = preloadItemsSignal
let queue = Queue.mainQueue()
self.additionalPreloadPeerIdsContext = QueueLocalObject(queue: queue, generate: {
AdditionalPreloadPeerIdsContext(queue: queue)
})
self.canPreloadHistoryDisposable = (networkState
|> map { state -> Bool in
switch state {
case .online:
return true
default:
return false
}
}
|> distinctUntilChanged
|> deliverOn(self.queue)).start(next: { [weak self] value in
guard let strongSelf = self, strongSelf.canPreloadHistoryValue != value else {
return
}
strongSelf.canPreloadHistoryValue = value
if value {
for i in 0 ..< min(3, strongSelf.entries.count) {
strongSelf.entries[i].startIfNeeded(postbox: strongSelf.postbox, accountPeerId: strongSelf.accountPeerId, download: strongSelf.download.get() |> take(1), queue: strongSelf.queue)
}
}
})
}
deinit {
self.canPreloadHistoryDisposable?.dispose()
}
func addAdditionalPeerId(peerId: PeerId) -> Disposable {
let disposable = MetaDisposable()
self.additionalPreloadPeerIdsContext.with { context in
disposable.set(context.add(peerId: peerId))
}
return disposable
}
func start() {
let additionalPreloadPeerIdsContext = self.additionalPreloadPeerIdsContext
let additionalPeerIds = Signal<Set<PeerId>, NoError> { subscriber in
let disposable = MetaDisposable()
additionalPreloadPeerIdsContext.with { context in
disposable.set(context.additionalPeerIds.start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
self.automaticChatListDisposable.set((combineLatest(queue: .mainQueue(), self.preloadItemsSignal, additionalPeerIds)
|> delay(1.0, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] loadItems, additionalPeerIds in
guard let strongSelf = self else {
return
}
/*#if DEBUG
if "".isEmpty {
return
}
#endif*/
var indices: [(ChatHistoryPreloadIndex, Bool, Bool)] = []
for item in loadItems {
indices.append((ChatHistoryPreloadIndex(index: item.index, entity: .peer(peerId: item.index.messageIndex.id.peerId, threadId: item.threadId)), item.hasUnread, item.isMuted))
}
strongSelf.update(indices: indices, additionalPeerIds: additionalPeerIds)
}))
}
private func update(indices: [(ChatHistoryPreloadIndex, Bool, Bool)], additionalPeerIds: Set<PeerId>) {
/*#if DEBUG
var indices = indices
indices.removeAll()
#endif*/
self.queue.async {
var validEntityIds = Set(indices.map { $0.0.entity })
for peerId in additionalPeerIds {
validEntityIds.insert(.peer(peerId: peerId, threadId: nil))
}
var removedEntityIds: [ChatHistoryPreloadEntity] = []
for (entityId, view) in self.views {
if !validEntityIds.contains(entityId) {
removedEntityIds.append(entityId)
if let hole = view.currentHole {
self.update(from: hole, to: nil)
}
}
}
for entityId in removedEntityIds {
self.views.removeValue(forKey: entityId)
}
var combinedIndices: [(ChatHistoryPreloadIndex, Bool, Bool, Bool)] = []
var existingPeerIds = Set<PeerId>()
for (index, hasUnread, isMuted) in indices {
existingPeerIds.insert(index.index.messageIndex.id.peerId)
combinedIndices.append((index, hasUnread, isMuted, additionalPeerIds.contains(index.index.messageIndex.id.peerId)))
}
for peerId in additionalPeerIds {
if !existingPeerIds.contains(peerId) {
combinedIndices.append((ChatHistoryPreloadIndex(index: ChatListIndex.absoluteLowerBound, entity: .peer(peerId: peerId, threadId: nil)), false, true, true))
}
}
for (index, hasUnread, isMuted, isPriority) in combinedIndices {
if let view = self.views[index.entity] {
if view.index != index.index || view.hasUnread != hasUnread || view.isMuted != isMuted {
let previousHole = view.currentHole
view.index = index.index
view.hasUnread = hasUnread
view.isMuted = isMuted
let updatedHole = view.currentHole
if previousHole != updatedHole {
self.update(from: previousHole, to: updatedHole)
}
}
} else {
var threadId: Int64?
switch index.entity {
case let .peer(_, threadIdValue):
threadId = threadIdValue
}
let view = HistoryPreloadViewContext(index: index.index, threadId: threadId, hasUnread: hasUnread, isMuted: isMuted, isPriority: isPriority)
self.views[index.entity] = view
let key: PostboxViewKey
switch index.entity {
case let .peer(peerId, threadId):
key = .messageOfInterestHole(location: .peer(peerId: peerId, threadId: threadId), namespace: Namespaces.Message.Cloud, count: 50)
}
view.disposable.set((self.postbox.combinedView(keys: [key])
|> deliverOn(self.queue)).start(next: { [weak self] next in
if let strongSelf = self, let value = next.views[key] as? MessageOfInterestHolesView {
if let view = strongSelf.views[index.entity] {
let previousHole = view.currentHole
view.hole = value.closestHole
var mediaUpdated = false
if view.media.count != value.closestLaterMedia.count {
mediaUpdated = true
} else {
for i in 0 ..< view.media.count {
if view.media[i] != value.closestLaterMedia[i] {
mediaUpdated = true
break
}
}
}
if mediaUpdated {
view.media = value.closestLaterMedia
strongSelf.updateMedia()
}
let updatedHole = view.currentHole
let holeIsUpdated = previousHole != updatedHole
switch index.entity {
case let .peer(peerId, threadId):
Logger.shared.log("HistoryPreload", "view \(peerId) (threadId: \(String(describing: threadId)) hole \(String(describing: updatedHole)) isUpdated: \(holeIsUpdated)")
}
if previousHole != updatedHole {
strongSelf.update(from: previousHole, to: updatedHole)
}
}
}
}))
}
}
}
}
private func updateMedia() {
var result: [ChatHistoryPreloadMediaItem] = []
for (_, view) in self.views {
for media in view.media {
result.append(ChatHistoryPreloadMediaItem(preloadIndex: view.preloadIndex, media: media))
}
}
result.sort()
if result != self.orderedMediaValue {
self.orderedMediaValue = result
self.orderedMediaPromise.set(result)
}
}
private func update(from previousHole: HistoryPreloadHole?, to updatedHole: HistoryPreloadHole?) {
assert(self.queue.isCurrent())
let isHoleUpdated = previousHole != updatedHole
Logger.shared.log("HistoryPreload", "update from \(String(describing: previousHole)) to \(String(describing: updatedHole)), isUpdated: \(isHoleUpdated)")
if !isHoleUpdated {
return
}
var skipUpdated = false
if let previousHole = previousHole {
for i in (0 ..< self.entries.count).reversed() {
if self.entries[i].hole == previousHole {
if let updatedHole = updatedHole, updatedHole.hole == self.entries[i].hole.hole {
self.entries[i].hole = updatedHole
skipUpdated = true
} else {
self.entries.remove(at: i)
}
break
}
}
}
if let updatedHole = updatedHole, !skipUpdated {
var found = false
for i in 0 ..< self.entries.count {
if self.entries[i].hole == updatedHole {
found = true
break
}
}
if !found {
self.entries.append(HistoryPreloadEntry(hole: updatedHole))
self.entries.sort()
}
}
if self.canPreloadHistoryValue {
Logger.shared.log("HistoryPreload", "will start")
for i in 0 ..< min(3, self.entries.count) {
self.entries[i].startIfNeeded(postbox: self.postbox, accountPeerId: self.accountPeerId, download: self.download.get() |> take(1), queue: self.queue)
}
} else {
Logger.shared.log("HistoryPreload", "will not start, canPreloadHistoryValue = false")
}
}
}
@@ -0,0 +1,51 @@
import Postbox
func cloudChatAddRemoveMessagesOperation(transaction: Transaction, peerId: PeerId, threadId: Int64?, messageIds: [MessageId], type: CloudChatRemoveMessagesType) {
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatRemoveMessagesOperation(messageIds: messageIds, threadId: threadId, type: type))
}
func cloudChatAddRemoveChatOperation(transaction: Transaction, peerId: PeerId, reportChatSpam: Bool, deleteGloballyIfPossible: Bool) {
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatRemoveChatOperation(peerId: peerId, reportChatSpam: reportChatSpam, deleteGloballyIfPossible: deleteGloballyIfPossible, topMessageId: transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud)))
}
func cloudChatAddClearHistoryOperation(transaction: Transaction, peerId: PeerId, threadId: Int64?, explicitTopMessageId: MessageId?, minTimestamp: Int32?, maxTimestamp: Int32?, type: CloudChatClearHistoryType) {
if type == .scheduledMessages {
var messageIds: [MessageId] = []
transaction.withAllMessages(peerId: peerId, namespace: Namespaces.Message.ScheduledCloud) { message -> Bool in
messageIds.append(message.id)
return true
}
cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: messageIds, type: .forLocalPeer)
} else if type == .quickReplyMessages {
var messageIds: [MessageId] = []
transaction.withAllMessages(peerId: peerId, namespace: Namespaces.Message.QuickReplyCloud) { message -> Bool in
messageIds.append(message.id)
return true
}
cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: messageIds, type: .forLocalPeer)
let topMessageId: MessageId?
if let explicitTopMessageId = explicitTopMessageId {
topMessageId = explicitTopMessageId
} else {
topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.QuickReplyCloud)
}
if let topMessageId = topMessageId {
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: topMessageId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type))
} else if case .forEveryone = type {
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: .max), threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type))
}
} else {
let topMessageId: MessageId?
if let explicitTopMessageId = explicitTopMessageId {
topMessageId = explicitTopMessageId
} else {
topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud)
}
if let topMessageId = topMessageId {
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: topMessageId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type))
} else if case .forEveryone = type {
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: .max), threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type))
}
}
}
@@ -0,0 +1,504 @@
import Foundation
import SwiftSignalKit
public protocol ConferenceCallE2EContextState: AnyObject {
func getEmojiState() -> Data?
func getParticipantIds() -> [Int64]
func getParticipants() -> [ConferenceCallE2EContext.BlockchainParticipant]
func getParticipantLatencies() -> [Int64: Double]
func applyBlock(block: Data) -> Bool
func applyBroadcastBlock(block: Data)
func generateRemoveParticipantsBlock(participantIds: [Int64]) -> Data?
func takeOutgoingBroadcastBlocks() -> [Data]
func encrypt(message: Data, channelId: Int32, plaintextPrefixLength: Int) -> Data?
func decrypt(message: Data, userId: Int64) -> Data?
}
public final class ConferenceCallE2EContext {
public final class ContextStateHolder {
public var state: ConferenceCallE2EContextState?
public var pendingIncomingBroadcastBlocks: [Data] = []
public init() {
}
}
public struct BlockchainParticipant: Equatable {
public let userId: Int64
public let internalId: String
public init(userId: Int64, internalId: String) {
self.userId = userId
self.internalId = internalId
}
}
private final class Impl {
private let queue: Queue
private let engine: TelegramEngine
private let callId: Int64
private let accessHash: Int64
private let userId: Int64
private let reference: InternalGroupCallReference
private let state: Atomic<ContextStateHolder>
private let initializeState: (TelegramKeyPair, Int64, Data) -> ConferenceCallE2EContextState?
private let keyPair: TelegramKeyPair
let e2eEncryptionKeyHashValue = ValuePromise<Data?>(nil)
let blockchainParticipantsValue = ValuePromise<[BlockchainParticipant]>([])
let isFailed = ValuePromise<Bool>(false, ignoreRepeated: true)
private var e2ePoll0Offset: Int?
private var e2ePoll0Timer: Foundation.Timer?
private var e2ePoll0Disposable: Disposable?
private var e2ePoll1Offset: Int?
private var e2ePoll1Timer: Foundation.Timer?
private var e2ePoll1Disposable: Disposable?
private var isSynchronizingRemovedParticipants: Bool = false
private var scheduledSynchronizeRemovedParticipants: Bool = false
private var scheduledSynchronizeRemovedParticipantsAfterPoll: Bool = false
private var synchronizeRemovedParticipantsDisposable: Disposable?
private var synchronizeRemovedParticipantsTimer: Foundation.Timer?
private var pendingKickPeers: [EnginePeer.Id] = []
init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64, userId: Int64, reference: InternalGroupCallReference, state: Atomic<ContextStateHolder>, initializeState: @escaping (TelegramKeyPair, Int64, Data) -> ConferenceCallE2EContextState?, keyPair: TelegramKeyPair) {
precondition(queue.isCurrent())
precondition(Queue.mainQueue().isCurrent())
self.queue = queue
self.engine = engine
self.callId = callId
self.accessHash = accessHash
self.userId = userId
self.reference = reference
self.state = state
self.initializeState = initializeState
self.keyPair = keyPair
}
deinit {
self.e2ePoll0Timer?.invalidate()
self.e2ePoll0Disposable?.dispose()
self.e2ePoll1Timer?.invalidate()
self.e2ePoll1Disposable?.dispose()
self.synchronizeRemovedParticipantsDisposable?.dispose()
self.synchronizeRemovedParticipantsTimer?.invalidate()
}
func begin(initialState: JoinGroupCallResult.E2EState?) {
self.scheduledSynchronizeRemovedParticipantsAfterPoll = true
self.synchronizeRemovedParticipantsTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true, block: { [weak self] _ in
guard let self else {
return
}
self.synchronizeRemovedParticipants()
})
if let initialState {
self.e2ePoll0Offset = initialState.subChain0.offset
self.e2ePoll1Offset = initialState.subChain1.offset
self.addE2EBlocks(blocks: initialState.subChain0.blocks, subChainId: 0)
self.addE2EBlocks(blocks: initialState.subChain1.blocks, subChainId: 1)
}
self.e2ePoll(subChainId: 0)
self.e2ePoll(subChainId: 1)
}
func addChainBlocksUpdate(subChainId: Int, blocks: [Data], nextOffset: Int) {
let updateBaseOffset = nextOffset - blocks.count
var blocksToProcess: [Data] = []
var shouldPoll = false
for i in 0 ..< blocks.count {
let blockOffset = updateBaseOffset + i
if subChainId == 0 {
if var e2ePoll0Offset = self.e2ePoll0Offset {
if blockOffset == e2ePoll0Offset {
e2ePoll0Offset += 1
self.e2ePoll0Offset = e2ePoll0Offset
blocksToProcess.append(blocks[i])
} else if blockOffset > e2ePoll0Offset {
shouldPoll = true
}
}
} else if subChainId == 1 {
if var e2ePoll1Offset = self.e2ePoll1Offset {
if blockOffset == e2ePoll1Offset {
e2ePoll1Offset += 1
self.e2ePoll1Offset = e2ePoll1Offset
blocksToProcess.append(blocks[i])
} else if blockOffset > e2ePoll1Offset {
shouldPoll = true
}
}
}
}
if !blocksToProcess.isEmpty {
if subChainId == 0 {
if self.e2ePoll0Disposable != nil {
self.e2ePoll0Disposable?.dispose()
self.e2ePoll0Disposable = nil
shouldPoll = true
}
} else if subChainId == 1 {
if self.e2ePoll1Disposable != nil {
self.e2ePoll1Disposable?.dispose()
self.e2ePoll1Disposable = nil
shouldPoll = true
}
}
self.addE2EBlocks(blocks: blocksToProcess, subChainId: subChainId)
}
if shouldPoll {
self.e2ePoll(subChainId: subChainId)
}
}
private func addE2EBlocks(blocks: [Data], subChainId: Int) {
let keyPair = self.keyPair
let userId = self.userId
let initializeState = self.initializeState
let (outBlocks, outEmoji, outBlockchainParticipants, participantLatencies, hadFailure) = self.state.with({ callState -> ([Data], Data, [BlockchainParticipant], [Int64: Double], Bool) in
if let state = callState.state {
var hadFailure = false
for block in blocks {
if subChainId == 0 {
if !state.applyBlock(block: block) {
hadFailure = true
}
} else if subChainId == 1 {
state.applyBroadcastBlock(block: block)
}
}
return (state.takeOutgoingBroadcastBlocks(), state.getEmojiState() ?? Data(), state.getParticipants(), state.getParticipantLatencies(), hadFailure)
} else {
if subChainId == 0 {
guard let block = blocks.last else {
return ([], Data(), [], [:], false)
}
guard let state = initializeState(keyPair, userId, block) else {
return ([], Data(), [], [:], false)
}
callState.state = state
for block in callState.pendingIncomingBroadcastBlocks {
state.applyBroadcastBlock(block: block)
}
callState.pendingIncomingBroadcastBlocks.removeAll()
return (state.takeOutgoingBroadcastBlocks(), state.getEmojiState() ?? Data(), state.getParticipants(), state.getParticipantLatencies(), false)
} else if subChainId == 1 {
callState.pendingIncomingBroadcastBlocks.append(contentsOf: blocks)
return ([], Data(), [], [:], false)
} else {
return ([], Data(), [], [:], false)
}
}
})
self.e2eEncryptionKeyHashValue.set(outEmoji.isEmpty ? nil : outEmoji)
self.blockchainParticipantsValue.set(outBlockchainParticipants)
for outBlock in outBlocks {
let _ = self.engine.calls.sendConferenceCallBroadcast(callId: self.callId, accessHash: self.accessHash, block: outBlock).startStandalone()
}
#if DEBUG
print("Latencies: \(participantLatencies)")
#endif
if hadFailure {
self.isFailed.set(true)
}
}
private func e2ePoll(subChainId: Int) {
let offset: Int?
if subChainId == 0 {
offset = self.e2ePoll0Offset
self.e2ePoll0Disposable?.dispose()
} else if subChainId == 1 {
offset = self.e2ePoll1Offset
self.e2ePoll1Disposable?.dispose()
} else {
return
}
let disposable = (self.engine.calls.pollConferenceCallBlockchain(reference: self.reference, subChainId: subChainId, offset: offset ?? 0, limit: 10)
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
guard let self else {
return
}
if subChainId == 0 {
self.e2ePoll0Disposable?.dispose()
self.e2ePoll0Disposable = nil
} else if subChainId == 1 {
self.e2ePoll1Disposable?.dispose()
self.e2ePoll1Disposable = nil
}
var delayPoll = true
if let result {
if subChainId == 0 {
if let e2ePoll0Offset = self.e2ePoll0Offset, e2ePoll0Offset < result.nextOffset {
self.e2ePoll0Offset = result.nextOffset
delayPoll = false
}
} else if subChainId == 1 {
if let e2ePoll1Offset = self.e2ePoll1Offset, e2ePoll1Offset < result.nextOffset {
self.e2ePoll1Offset = result.nextOffset
delayPoll = false
}
}
self.addE2EBlocks(blocks: result.blocks, subChainId: subChainId)
}
if subChainId == 0 {
self.e2ePoll0Timer?.invalidate()
self.e2ePoll0Timer = Foundation.Timer.scheduledTimer(withTimeInterval: delayPoll ? 1.0 : 0.0, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.e2ePoll(subChainId: 0)
})
if self.scheduledSynchronizeRemovedParticipantsAfterPoll {
self.scheduledSynchronizeRemovedParticipantsAfterPoll = false
self.synchronizeRemovedParticipants()
}
} else if subChainId == 1 {
self.e2ePoll1Timer?.invalidate()
self.e2ePoll1Timer = Foundation.Timer.scheduledTimer(withTimeInterval: delayPoll ? 1.0 : 0.0, repeats: false, block: { [weak self] _ in
guard let self else {
return
}
self.e2ePoll(subChainId: 1)
})
}
})
if subChainId == 0 {
self.e2ePoll0Disposable = disposable
} else if subChainId == 1 {
self.e2ePoll1Disposable = disposable
}
}
func synchronizeRemovedParticipants() {
if self.isSynchronizingRemovedParticipants {
self.scheduledSynchronizeRemovedParticipants = true
return
}
self.isSynchronizingRemovedParticipants = true
let engine = self.engine
let state = self.state
let callId = self.callId
let accessHash = self.accessHash
let accountPeerId = engine.account.peerId
if !self.pendingKickPeers.isEmpty {
let pendingKickPeers = self.pendingKickPeers
self.pendingKickPeers.removeAll()
self.synchronizeRemovedParticipantsDisposable?.dispose()
let removeBlock = state.with({ state -> Data? in
guard let state = state.state else {
return nil
}
let currentIds = state.getParticipantIds()
let remainingIds = pendingKickPeers.filter({ currentIds.contains($0.id._internalGetInt64Value()) })
if remainingIds.isEmpty {
return nil
}
return state.generateRemoveParticipantsBlock(participantIds: remainingIds.map { $0.id._internalGetInt64Value() })
})
if let removeBlock {
self.synchronizeRemovedParticipantsDisposable = (engine.calls.removeGroupCallBlockchainParticipants(callId: callId, accessHash: accessHash, mode: .kick, participantIds: pendingKickPeers.map { $0.id._internalGetInt64Value() }, block: removeBlock)
|> map { result -> Bool in
switch result {
case .success:
return true
case .pollBlocksAndRetry:
return false
}
}
|> deliverOnMainQueue).startStrict(next: { [weak self] shouldRetry in
guard let self else {
return
}
if shouldRetry {
for id in pendingKickPeers {
if !self.pendingKickPeers.contains(id) {
self.pendingKickPeers.append(id)
}
}
}
self.isSynchronizingRemovedParticipants = false
if self.scheduledSynchronizeRemovedParticipants {
self.scheduledSynchronizeRemovedParticipants = false
self.synchronizeRemovedParticipants()
} else if shouldRetry && !self.scheduledSynchronizeRemovedParticipantsAfterPoll {
self.scheduledSynchronizeRemovedParticipantsAfterPoll = true
self.e2ePoll(subChainId: 0)
}
})
} else {
self.isSynchronizingRemovedParticipants = false
if self.scheduledSynchronizeRemovedParticipants {
self.scheduledSynchronizeRemovedParticipants = false
self.synchronizeRemovedParticipants()
}
}
} else {
self.synchronizeRemovedParticipantsDisposable?.dispose()
self.synchronizeRemovedParticipantsDisposable = (_internal_getGroupCallParticipants(
account: self.engine.account,
reference: self.reference,
offset: "",
ssrcs: [],
limit: 100,
sortAscending: true,
isStream: false
)
|> map(Optional.init)
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Bool, NoError> in
guard let result else {
return .single(false)
}
let blockchainPeerIds = state.with { state -> [Int64] in
guard let state = state.state else {
return []
}
return state.getParticipantIds()
}
// Peer ids that are in the blockchain but not in the server list
var removedPeerIds = blockchainPeerIds.filter { blockchainPeerId in
return !result.participants.contains(where: { participant in
if case let .peer(id) = participant.id, id.namespace == Namespaces.Peer.CloudUser, id.id._internalGetInt64Value() == blockchainPeerId {
return true
} else {
return false
}
})
}
removedPeerIds.removeAll(where: { $0 == accountPeerId.id._internalGetInt64Value() })
if removedPeerIds.isEmpty {
return .single(false)
}
guard let removeBlock = state.with({ state -> Data? in
guard let state = state.state else {
return nil
}
return state.generateRemoveParticipantsBlock(participantIds: removedPeerIds)
}) else {
return .single(false)
}
return engine.calls.removeGroupCallBlockchainParticipants(callId: callId, accessHash: accessHash, mode: .cleanup, participantIds: removedPeerIds, block: removeBlock)
|> map { result -> Bool in
switch result {
case .success:
return true
case .pollBlocksAndRetry:
return false
}
}
}
|> deliverOn(self.queue)).startStrict(next: { [weak self] shouldRetry in
guard let self else {
return
}
self.isSynchronizingRemovedParticipants = false
if self.scheduledSynchronizeRemovedParticipants {
self.scheduledSynchronizeRemovedParticipants = false
self.synchronizeRemovedParticipants()
} else if shouldRetry && !self.scheduledSynchronizeRemovedParticipantsAfterPoll {
self.scheduledSynchronizeRemovedParticipantsAfterPoll = true
self.e2ePoll(subChainId: 0)
}
})
}
}
func kickPeer(id: EnginePeer.Id) {
if !self.pendingKickPeers.contains(id) {
self.pendingKickPeers.append(id)
self.synchronizeRemovedParticipants()
}
}
}
public let state: Atomic<ContextStateHolder> = Atomic(value: ContextStateHolder())
private let impl: QueueLocalObject<Impl>
public var e2eEncryptionKeyHash: Signal<Data?, NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.e2eEncryptionKeyHashValue.get().start(next: subscriber.putNext)
}
}
public var blockchainParticipants: Signal<[BlockchainParticipant], NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.blockchainParticipantsValue.get().start(next: subscriber.putNext)
}
}
public var isFailed: Signal<Bool, NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.isFailed.get().start(next: subscriber.putNext)
}
}
public init(engine: TelegramEngine, callId: Int64, accessHash: Int64, userId: Int64, reference: InternalGroupCallReference, keyPair: TelegramKeyPair, initializeState: @escaping (TelegramKeyPair, Int64, Data) -> ConferenceCallE2EContextState?) {
let queue = Queue.mainQueue()
let state = self.state
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, engine: engine, callId: callId, accessHash: accessHash, userId: userId, reference: reference, state: state, initializeState: initializeState, keyPair: keyPair)
})
}
public func begin(initialState: JoinGroupCallResult.E2EState?) {
self.impl.with { impl in
impl.begin(initialState: initialState)
}
}
public func addChainBlocksUpdate(subChainId: Int, blocks: [Data], nextOffset: Int) {
self.impl.with { impl in
impl.addChainBlocksUpdate(subChainId: subChainId, blocks: blocks, nextOffset: nextOffset)
}
}
public func synchronizeRemovedParticipants() {
self.impl.with { impl in
impl.synchronizeRemovedParticipants()
}
}
public func kickPeer(id: EnginePeer.Id) {
self.impl.with { impl in
impl.kickPeer(id: id)
}
}
}
@@ -0,0 +1,440 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
private func normalizedPhoneNumber(_ value: String) -> String {
var result = ""
for c in value {
if c.isNumber {
result.append(c)
}
}
return result
}
private final class ContactSyncOperation {
let id: Int32
var isRunning: Bool = false
let content: ContactSyncOperationContent
let disposable = DisposableSet()
init(id: Int32, content: ContactSyncOperationContent) {
self.id = id
self.content = content
}
}
private enum ContactSyncOperationContent {
case waitForUpdatedState
case updatePresences
case sync(importableContacts: [DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]?)
case updateIsContact([(PeerId, Bool)])
}
private final class ContactSyncManagerImpl {
private let queue: Queue
private let postbox: Postbox
private let network: Network
private let accountPeerId: PeerId
private let stateManager: AccountStateManager
private var nextId: Int32 = 0
private var operations: [ContactSyncOperation] = []
private var lastContactPresencesRequestTimestamp: Double?
private var reimportAttempts: [TelegramDeviceContactImportIdentifier: Double] = [:]
private let importableContactsDisposable = MetaDisposable()
private let significantStateUpdateCompletedDisposable = MetaDisposable()
init(queue: Queue, postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager) {
self.queue = queue
self.postbox = postbox
self.network = network
self.accountPeerId = accountPeerId
self.stateManager = stateManager
}
deinit {
self.importableContactsDisposable.dispose()
}
func beginSync(importableContacts: Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError>) {
self.importableContactsDisposable.set((importableContacts
|> deliverOn(self.queue)).start(next: { [weak self] importableContacts in
guard let strongSelf = self else {
return
}
strongSelf.addOperation(.waitForUpdatedState)
strongSelf.addOperation(.updatePresences)
strongSelf.addOperation(.sync(importableContacts: importableContacts))
}))
self.significantStateUpdateCompletedDisposable.set((self.stateManager.significantStateUpdateCompleted
|> deliverOn(self.queue)).start(next: { [weak self] in
guard let strongSelf = self else {
return
}
let timestamp = CFAbsoluteTimeGetCurrent()
let shouldUpdate: Bool
if let lastContactPresencesRequestTimestamp = strongSelf.lastContactPresencesRequestTimestamp {
if timestamp > lastContactPresencesRequestTimestamp + 2.0 * 60.0 {
shouldUpdate = true
} else {
shouldUpdate = false
}
} else {
shouldUpdate = true
}
if shouldUpdate {
strongSelf.lastContactPresencesRequestTimestamp = timestamp
var found = false
for operation in strongSelf.operations {
if case .updatePresences = operation.content {
found = true
break
}
}
if !found {
strongSelf.addOperation(.updatePresences)
}
}
}))
}
func addIsContactUpdates(_ updates: [(PeerId, Bool)]) {
self.addOperation(.updateIsContact(updates))
}
func addOperation(_ content: ContactSyncOperationContent) {
let id = self.nextId
self.nextId += 1
let operation = ContactSyncOperation(id: id, content: content)
switch content {
case .waitForUpdatedState:
self.operations.append(operation)
case .updatePresences:
for i in (0 ..< self.operations.count).reversed() {
if case .updatePresences = self.operations[i].content {
if !self.operations[i].isRunning {
self.operations.remove(at: i)
}
}
}
self.operations.append(operation)
case .sync:
for i in (0 ..< self.operations.count).reversed() {
if case .sync = self.operations[i].content {
if !self.operations[i].isRunning {
self.operations.remove(at: i)
}
}
}
self.operations.append(operation)
case let .updateIsContact(updates):
var mergedUpdates: [(PeerId, Bool)] = []
var removeIndices: [Int] = []
for i in 0 ..< self.operations.count {
if case let .updateIsContact(operationUpdates) = self.operations[i].content {
if !self.operations[i].isRunning {
mergedUpdates.append(contentsOf: operationUpdates)
removeIndices.append(i)
}
}
}
mergedUpdates.append(contentsOf: updates)
for index in removeIndices.reversed() {
self.operations.remove(at: index)
}
if self.operations.isEmpty || !self.operations[0].isRunning {
self.operations.insert(operation, at: 0)
} else {
self.operations.insert(operation, at: 1)
}
}
self.updateOperations()
}
func updateOperations() {
if let first = self.operations.first, !first.isRunning {
first.isRunning = true
let id = first.id
let queue = self.queue
self.startOperation(first.content, disposable: first.disposable, completion: { [weak self] in
queue.async {
guard let strongSelf = self else {
return
}
if let currentFirst = strongSelf.operations.first, currentFirst.id == id {
strongSelf.operations.remove(at: 0)
strongSelf.updateOperations()
} else {
assertionFailure()
}
}
})
}
}
func startOperation(_ operation: ContactSyncOperationContent, disposable: DisposableSet, completion: @escaping () -> Void) {
switch operation {
case .waitForUpdatedState:
disposable.add((self.stateManager.isUpdating
|> filter { !$0 }
|> take(1)
|> deliverOn(self.queue)).start(next: { _ in
completion()
}))
case .updatePresences:
disposable.add(updateContactPresences(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId).start(completed: {
completion()
}))
case let .sync(importableContacts):
let importSignal: Signal<PushDeviceContactsResult, NoError>
if let importableContacts = importableContacts {
importSignal = pushDeviceContacts(accountPeerId: self.accountPeerId, postbox: self.postbox, network: self.network, importableContacts: importableContacts, reimportAttempts: self.reimportAttempts)
} else {
importSignal = .single(PushDeviceContactsResult(addedReimportAttempts: [:]))
}
disposable.add(
(syncContactsOnce(network: self.network, postbox: self.postbox, accountPeerId: self.accountPeerId)
|> mapToSignal { _ -> Signal<PushDeviceContactsResult, NoError> in
}
|> then(importSignal)
|> deliverOn(self.queue)
).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
for (identifier, timestamp) in result.addedReimportAttempts {
strongSelf.reimportAttempts[identifier] = timestamp
}
completion()
}))
case let .updateIsContact(updates):
disposable.add((self.postbox.transaction { transaction -> Void in
var contactPeerIds = transaction.getContactPeerIds()
for (peerId, isContact) in updates {
if isContact {
contactPeerIds.insert(peerId)
} else {
contactPeerIds.remove(peerId)
}
}
transaction.replaceContactPeerIds(contactPeerIds)
}
|> deliverOnMainQueue).start(completed: {
completion()
}))
}
}
}
private struct PushDeviceContactsResult {
let addedReimportAttempts: [TelegramDeviceContactImportIdentifier: Double]
}
private func pushDeviceContacts(accountPeerId: PeerId, postbox: Postbox, network: Network, importableContacts: [DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], reimportAttempts: [TelegramDeviceContactImportIdentifier: Double]) -> Signal<PushDeviceContactsResult, NoError> {
return postbox.transaction { transaction -> Signal<PushDeviceContactsResult, NoError> in
var noLongerImportedIdentifiers = Set<TelegramDeviceContactImportIdentifier>()
var updatedDataIdentifiers = Set<TelegramDeviceContactImportIdentifier>()
var addedIdentifiers = Set<TelegramDeviceContactImportIdentifier>()
var retryLaterIdentifiers = Set<TelegramDeviceContactImportIdentifier>()
addedIdentifiers.formUnion(importableContacts.keys.map(TelegramDeviceContactImportIdentifier.phoneNumber))
transaction.enumerateDeviceContactImportInfoItems({ key, value in
if let identifier = TelegramDeviceContactImportIdentifier(key: key) {
addedIdentifiers.remove(identifier)
switch identifier {
case let .phoneNumber(number):
if let updatedData = importableContacts[number] {
if let value = value as? TelegramDeviceContactImportedData {
switch value {
case let .imported(data, _, _):
if data != updatedData {
updatedDataIdentifiers.insert(identifier)
}
case .retryLater:
retryLaterIdentifiers.insert(identifier)
}
} else {
assertionFailure()
}
} else {
noLongerImportedIdentifiers.insert(identifier)
}
}
} else {
assertionFailure()
}
return true
})
for identifier in noLongerImportedIdentifiers {
transaction.setDeviceContactImportInfo(identifier.key, value: nil)
}
var orderedPushIdentifiers: [TelegramDeviceContactImportIdentifier] = []
orderedPushIdentifiers.append(contentsOf: addedIdentifiers.sorted())
orderedPushIdentifiers.append(contentsOf: updatedDataIdentifiers.sorted())
orderedPushIdentifiers.append(contentsOf: retryLaterIdentifiers.sorted())
var currentContactDetails: [TelegramDeviceContactImportIdentifier: TelegramUser] = [:]
for peerId in transaction.getContactPeerIds() {
if let user = transaction.getPeer(peerId) as? TelegramUser, let phone = user.phone, !phone.isEmpty {
currentContactDetails[.phoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: normalizedPhoneNumber(phone)))] = user
}
}
let timestamp = CFAbsoluteTimeGetCurrent()
outer: for i in (0 ..< orderedPushIdentifiers.count).reversed() {
if let user = currentContactDetails[orderedPushIdentifiers[i]], case let .phoneNumber(number) = orderedPushIdentifiers[i], let data = importableContacts[number] {
if (user.firstName ?? "") == data.firstName && (user.lastName ?? "") == data.lastName {
if data.localIdentifiers.contains("5DFF1D6F-8C0A-48C9-800D-F4BEC59C0E50") {
assert(true)
}
transaction.setDeviceContactImportInfo(orderedPushIdentifiers[i].key, value: TelegramDeviceContactImportedData.imported(data: data, importedByCount: 0, peerId: user.id))
orderedPushIdentifiers.remove(at: i)
continue outer
}
}
if let attemptTimestamp = reimportAttempts[orderedPushIdentifiers[i]], attemptTimestamp + 60.0 * 60.0 * 24.0 > timestamp {
orderedPushIdentifiers.remove(at: i)
}
}
var preparedContactData: [(DeviceContactNormalizedPhoneNumber, ImportableDeviceContactData)] = []
for identifier in orderedPushIdentifiers {
if case let .phoneNumber(number) = identifier, let value = importableContacts[number] {
preparedContactData.append((number, value))
}
}
return pushDeviceContactData(accountPeerId: accountPeerId, postbox: postbox, network: network, contacts: preparedContactData)
}
|> switchToLatest
}
private let importBatchCount: Int = 500
private func pushDeviceContactData(accountPeerId: PeerId, postbox: Postbox, network: Network, contacts: [(DeviceContactNormalizedPhoneNumber, ImportableDeviceContactData)]) -> Signal<PushDeviceContactsResult, NoError> {
var batches: Signal<PushDeviceContactsResult, NoError> = .single(PushDeviceContactsResult(addedReimportAttempts: [:]))
for s in stride(from: 0, to: contacts.count, by: importBatchCount) {
let batch = Array(contacts[s ..< min(s + importBatchCount, contacts.count)])
batches = batches
|> mapToSignal { intermediateResult -> Signal<PushDeviceContactsResult, NoError> in
return network.request(Api.functions.contacts.importContacts(contacts: zip(0 ..< batch.count, batch).map { index, item -> Api.InputContact in
return .inputPhoneContact(flags: 0, clientId: Int64(index), phone: item.0.rawValue, firstName: item.1.firstName, lastName: item.1.lastName, note: nil)
}))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.contacts.ImportedContacts?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<PushDeviceContactsResult, NoError> in
return postbox.transaction { transaction -> PushDeviceContactsResult in
var addedReimportAttempts: [TelegramDeviceContactImportIdentifier: Double] = intermediateResult.addedReimportAttempts
if let result = result {
var addedContactPeerIds = Set<PeerId>()
var retryIndices = Set<Int>()
var importedCounts: [Int: Int32] = [:]
var peerIdByClientId: [Int64: PeerId] = [:]
switch result {
case let .importedContacts(imported, popularInvites, retryContacts, users):
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
for item in imported {
switch item {
case let .importedContact(userId, clientId):
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
addedContactPeerIds.insert(peerId)
peerIdByClientId[clientId] = peerId
}
}
for item in retryContacts {
retryIndices.insert(Int(item))
}
for item in popularInvites {
switch item {
case let .popularContact(clientId, importers):
importedCounts[Int(clientId)] = importers
}
}
}
let timestamp = CFAbsoluteTimeGetCurrent()
for i in 0 ..< batch.count {
let importedData: TelegramDeviceContactImportedData
if retryIndices.contains(i) {
importedData = .retryLater
addedReimportAttempts[.phoneNumber(batch[i].0)] = timestamp
} else {
if batch[i].1.localIdentifiers.contains("5DFF1D6F-8C0A-48C9-800D-F4BEC59C0E50") {
assert(true)
}
importedData = .imported(data: batch[i].1, importedByCount: importedCounts[i] ?? 0, peerId: peerIdByClientId[Int64(i)])
}
transaction.setDeviceContactImportInfo(TelegramDeviceContactImportIdentifier.phoneNumber(batch[i].0).key, value: importedData)
}
var contactPeerIds = transaction.getContactPeerIds()
contactPeerIds.formUnion(addedContactPeerIds)
transaction.replaceContactPeerIds(contactPeerIds)
} else {
let timestamp = CFAbsoluteTimeGetCurrent()
for (number, _) in batch {
addedReimportAttempts[.phoneNumber(number)] = timestamp
transaction.setDeviceContactImportInfo(TelegramDeviceContactImportIdentifier.phoneNumber(number).key, value: TelegramDeviceContactImportedData.retryLater)
}
}
return PushDeviceContactsResult(addedReimportAttempts: addedReimportAttempts)
}
}
}
}
return batches
}
private func updateContactPresences(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal<Never, NoError> {
return network.request(Api.functions.contacts.getStatuses())
|> `catch` { _ -> Signal<[Api.ContactStatus], NoError> in
return .single([])
}
|> mapToSignal { statuses -> Signal<Never, NoError> in
return postbox.transaction { transaction -> Void in
var peerPresences: [PeerId: PeerPresence] = [:]
for status in statuses {
switch status {
case let .contactStatus(userId, status):
peerPresences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))] = TelegramUserPresence(apiStatus: status)
}
}
updatePeerPresencesClean(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences)
}
|> ignoreValues
}
}
final class ContactSyncManager {
private let queue = Queue()
private let impl: QueueLocalObject<ContactSyncManagerImpl>
init(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return ContactSyncManagerImpl(queue: queue, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager)
})
}
func beginSync(importableContacts: Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError>) {
self.impl.with { impl in
impl.beginSync(importableContacts: importableContacts)
}
}
func addIsContactUpdates(_ updates: [(PeerId, Bool)]) {
self.impl.with { impl in
impl.addIsContactUpdates(updates)
}
}
}
@@ -0,0 +1,233 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
public final class EmojiSearchCategories: Equatable, Codable {
public enum Kind: Int64 {
case emoji = 0
case status = 1
case avatar = 2
case combinedChatStickers = 3
}
public struct Group: Codable, Equatable {
enum CodingKeys: String, CodingKey {
case id
case title
case identifiers
case kind
}
public enum Kind: Int32, Codable {
case generic
case greeting
case premium
}
public var id: Int64
public var title: String
public var identifiers: [String]
public var kind: Kind
public init(id: Int64, title: String, identifiers: [String], kind: Kind) {
self.id = id
self.title = title
self.identifiers = identifiers
self.kind = kind
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
self.title = try container.decode(String.self, forKey: .title)
self.identifiers = try container.decode([String].self, forKey: .identifiers)
self.kind = ((try container.decodeIfPresent(Int32.self, forKey: .kind)).flatMap(Kind.init(rawValue:))) ?? .generic
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.title, forKey: .title)
try container.encode(self.identifiers, forKey: .identifiers)
try container.encode(self.kind.rawValue, forKey: .kind)
}
}
private enum CodingKeys: String, CodingKey {
case newHash
case groups
}
public let hash: Int32
public let groups: [Group]
public init(
hash: Int32,
groups: [Group]
) {
self.hash = hash
self.groups = groups
}
public static func ==(lhs: EmojiSearchCategories, rhs: EmojiSearchCategories) -> Bool {
if lhs === rhs {
return true
}
if lhs.hash != rhs.hash {
return false
}
if lhs.groups != rhs.groups {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.hash = try container.decodeIfPresent(Int32.self, forKey: .newHash) ?? 0
self.groups = try container.decode([Group].self, forKey: .groups)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.hash, forKey: .newHash)
try container.encode(self.groups, forKey: .groups)
}
}
func _internal_cachedEmojiSearchCategories(postbox: Postbox, kind: EmojiSearchCategories.Kind) -> Signal<EmojiSearchCategories?, NoError> {
return postbox.transaction { transaction -> EmojiSearchCategories? in
return _internal_cachedEmojiSearchCategories(transaction: transaction, kind: kind)
}
}
func _internal_cachedEmojiSearchCategories(transaction: Transaction, kind: EmojiSearchCategories.Kind) -> EmojiSearchCategories? {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: kind.rawValue)
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.emojiSearchCategories, key: key))?.get(EmojiSearchCategories.self)
if let cached = cached {
return cached
} else {
return nil
}
}
func _internal_setCachedEmojiSearchCategories(transaction: Transaction, categories: EmojiSearchCategories, kind: EmojiSearchCategories.Kind) {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: kind.rawValue)
if let entry = CodableEntry(categories) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.emojiSearchCategories, key: key), entry: entry)
}
}
func managedSynchronizeEmojiSearchCategories(postbox: Postbox, network: Network, kind: EmojiSearchCategories.Kind) -> Signal<Never, NoError> {
let poll = Signal<Never, NoError> { subscriber in
let signal: Signal<Never, NoError> = _internal_cachedEmojiSearchCategories(postbox: postbox, kind: kind)
|> mapToSignal { current in
let signal: Signal<Api.messages.EmojiGroups, NoError>
switch kind {
case .emoji:
signal = network.request(Api.functions.messages.getEmojiGroups(hash: current?.hash ?? 0))
|> `catch` { _ -> Signal<Api.messages.EmojiGroups, NoError> in
return .single(.emojiGroupsNotModified)
}
case .status:
signal = network.request(Api.functions.messages.getEmojiStatusGroups(hash: current?.hash ?? 0))
|> `catch` { _ -> Signal<Api.messages.EmojiGroups, NoError> in
return .single(.emojiGroupsNotModified)
}
case .avatar:
signal = network.request(Api.functions.messages.getEmojiProfilePhotoGroups(hash: current?.hash ?? 0))
|> `catch` { _ -> Signal<Api.messages.EmojiGroups, NoError> in
return .single(.emojiGroupsNotModified)
}
case .combinedChatStickers:
signal = network.request(Api.functions.messages.getEmojiStickerGroups(hash: current?.hash ?? 0))
|> `catch` { _ -> Signal<Api.messages.EmojiGroups, NoError> in
return .single(.emojiGroupsNotModified)
}
}
return signal
|> mapToSignal { result -> Signal<Never, NoError> in
return postbox.transaction { transaction -> Signal<Never, NoError> in
switch result {
case let .emojiGroups(hash, groups):
let categories = EmojiSearchCategories(
hash: hash,
groups: groups.compactMap { item -> EmojiSearchCategories.Group? in
switch item {
case let .emojiGroup(title, iconEmojiId, emoticons):
return EmojiSearchCategories.Group(
id: iconEmojiId,
title: title,
identifiers: emoticons,
kind: .generic
)
case let .emojiGroupGreeting(title, iconEmojiId, emoticons):
return EmojiSearchCategories.Group(
id: iconEmojiId,
title: title,
identifiers: emoticons,
kind: .greeting
)
case let .emojiGroupPremium(title, iconEmojiId):
return EmojiSearchCategories.Group(
id: iconEmojiId,
title: title,
identifiers: [],
kind: .premium
)
}
}
)
_internal_setCachedEmojiSearchCategories(transaction: transaction, categories: categories, kind: kind)
case .emojiGroupsNotModified:
break
}
var fileIds: [Int64] = []
if let cached = _internal_cachedEmojiSearchCategories(transaction: transaction, kind: kind) {
for group in cached.groups {
fileIds.append(group.id)
}
}
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: fileIds)
|> mapToSignal { files -> Signal<Never, NoError> in
var fetchSignals: Signal<Never, NoError> = .complete()
for (_, file) in files {
let signal = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .standalone(resource: file.resource))
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
}
fetchSignals = fetchSignals |> then(signal)
}
return fetchSignals
}
}
|> switchToLatest
}
}
return signal.start(completed: {
subscriber.putCompletion()
})
}
return (
poll
|> then(
.complete()
|> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue())
)
)
|> restart
}
@@ -0,0 +1,176 @@
import Foundation
import Postbox
import SwiftSignalKit
#if os(iOS)
import Photos
#endif
private final class MediaResourceDataCopyFile : MediaResourceDataFetchCopyLocalItem {
let path: String
init(path: String) {
self.path = path
}
func copyTo(url: URL) -> Bool {
do {
try FileManager.default.copyItem(at: URL(fileURLWithPath: self.path), to: url)
return true
} catch {
return false
}
}
}
func fetchCloudMediaLocation(
accountPeerId: PeerId,
postbox: Postbox,
network: Network,
mediaReferenceRevalidationContext: MediaReferenceRevalidationContext,
networkStatsContext: NetworkStatsContext,
resource: TelegramMediaResource,
datacenterId: Int,
size: Int64?,
intervals: Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError>,
parameters: MediaResourceFetchParameters?
) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
return multipartFetch(
accountPeerId: accountPeerId,
postbox: postbox,
network: network,
mediaReferenceRevalidationContext: mediaReferenceRevalidationContext,
networkStatsContext: networkStatsContext,
resource: resource,
datacenterId: datacenterId,
size: size,
intervals: intervals,
parameters: parameters
)
}
private func fetchLocalFileResource(path: String, move: Bool) -> Signal<MediaResourceDataFetchResult, NoError> {
return Signal { subscriber in
if move {
subscriber.putNext(.moveLocalFile(path: path))
subscriber.putCompletion()
} else {
subscriber.putNext(.copyLocalItem(MediaResourceDataCopyFile(path: path)))
subscriber.putCompletion()
}
return EmptyDisposable
}
}
func fetchResource(account: Account, resource: MediaResource, intervals: Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError>, parameters: MediaResourceFetchParameters?) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>? {
return fetchResource(
accountPeerId: account.peerId,
postbox: account.postbox,
network: account.network,
mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext,
networkStatsContext: account.networkStatsContext,
isTestingEnvironment: account.testingEnvironment,
resource: resource,
intervals: intervals,
parameters: parameters
)
}
func fetchResource(
accountPeerId: PeerId,
postbox: Postbox,
network: Network,
mediaReferenceRevalidationContext: MediaReferenceRevalidationContext,
networkStatsContext: NetworkStatsContext,
isTestingEnvironment: Bool,
resource: MediaResource,
intervals: Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError>,
parameters: MediaResourceFetchParameters?
) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>? {
if let _ = resource as? EmptyMediaResource {
return .single(.reset)
|> then(.never())
} else if let secretFileResource = resource as? SecretFileMediaResource {
return .single(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: false))
|> then(fetchSecretFileResource(
accountPeerId: accountPeerId,
postbox: postbox,
network: network,
mediaReferenceRevalidationContext: mediaReferenceRevalidationContext,
networkStatsContext: networkStatsContext,
resource: secretFileResource,
intervals: intervals,
parameters: parameters
))
} else if let cloudResource = resource as? TelegramMultipartFetchableResource {
return .single(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: false))
|> then(fetchCloudMediaLocation(
accountPeerId: accountPeerId,
postbox: postbox,
network: network,
mediaReferenceRevalidationContext: mediaReferenceRevalidationContext,
networkStatsContext: networkStatsContext,
resource: cloudResource,
datacenterId: cloudResource.datacenterId,
size: resource.size == 0 ? nil : resource.size,
intervals: intervals,
parameters: parameters
))
} else if let webFileResource = resource as? MediaResourceWithWebFileReference {
return currentWebDocumentsHostDatacenterId(postbox: postbox, isTestingEnvironment: isTestingEnvironment)
|> castError(MediaResourceDataFetchError.self)
|> mapToSignal { datacenterId -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
return .single(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: false))
|> then(fetchCloudMediaLocation(
accountPeerId: accountPeerId,
postbox: postbox,
network: network,
mediaReferenceRevalidationContext: mediaReferenceRevalidationContext,
networkStatsContext: networkStatsContext,
resource: webFileResource,
datacenterId: Int(datacenterId),
size: resource.size == 0 ? nil : resource.size,
intervals: intervals,
parameters: parameters
))
}
} else if let localFileResource = resource as? LocalFileReferenceMediaResource {
return fetchLocalFileResource(path: localFileResource.localFilePath, move: localFileResource.isUniquelyReferencedTemporaryFile)
|> castError(MediaResourceDataFetchError.self)
} else if let httpReference = resource as? HttpReferenceMediaResource {
return .single(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: false))
|> then(fetchHttpResource(url: httpReference.url))
} else if let wallpaperResource = resource as? WallpaperDataResource {
return getWallpaper(network: network, slug: wallpaperResource.slug)
|> mapError { _ -> MediaResourceDataFetchError in
return .generic
}
|> mapToSignal { wallpaper -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
guard case let .file(file) = wallpaper else {
return .fail(.generic)
}
guard let cloudResource = file.file.resource as? TelegramMultipartFetchableResource else {
return .fail(.generic)
}
return .single(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: false))
|> then(fetchCloudMediaLocation(
accountPeerId: accountPeerId,
postbox: postbox,
network: network,
mediaReferenceRevalidationContext: mediaReferenceRevalidationContext,
networkStatsContext: networkStatsContext,
resource: cloudResource,
datacenterId: cloudResource.datacenterId,
size: resource.size == 0 ? nil : resource.size,
intervals: intervals,
parameters: MediaResourceFetchParameters(
tag: nil,
info: TelegramCloudMediaResourceFetchInfo(reference: .standalone(resource: file.file.resource), preferBackgroundReferenceRevalidation: false, continueInBackground: false),
location: nil,
contentType: .other,
isRandomAccessAllowed: true
)
))
}
}
return nil
}
@@ -0,0 +1,411 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
enum FetchChatListLocation {
case general
case group(PeerGroupId)
}
struct ParsedDialogs {
let itemIds: [PeerId]
let peers: AccumulatedPeers
let notificationSettings: [PeerId: PeerNotificationSettings]
let readStates: [PeerId: [MessageId.Namespace: PeerReadState]]
let mentionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary]
let reactionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary]
let channelStates: [PeerId: Int32]
let topMessageIds: [PeerId: MessageId]
let storeMessages: [StoreMessage]
let ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout]
let viewForumAsMessages: [PeerId: Bool]
let lowerNonPinnedIndex: MessageIndex?
let referencedFolders: [PeerGroupId: PeerGroupUnreadCountersSummary]
}
private func extractDialogsData(dialogs: Api.messages.Dialogs) -> (apiDialogs: [Api.Dialog], apiMessages: [Api.Message], apiChats: [Api.Chat], apiUsers: [Api.User], apiIsAtLowestBoundary: Bool) {
switch dialogs {
case let .dialogs(dialogs, messages, chats, users):
return (dialogs, messages, chats, users, true)
case let .dialogsSlice(_, dialogs, messages, chats, users):
return (dialogs, messages, chats, users, false)
case .dialogsNotModified:
assertionFailure()
return ([], [], [], [], true)
}
}
private func extractDialogsData(peerDialogs: Api.messages.PeerDialogs) -> (apiDialogs: [Api.Dialog], apiMessages: [Api.Message], apiChats: [Api.Chat], apiUsers: [Api.User], apiIsAtLowestBoundary: Bool) {
switch peerDialogs {
case let .peerDialogs(dialogs, messages, chats, users, _):
return (dialogs, messages, chats, users, false)
}
}
private func parseDialogs(accountPeerId: PeerId, apiDialogs: [Api.Dialog], apiMessages: [Api.Message], apiChats: [Api.Chat], apiUsers: [Api.User], apiIsAtLowestBoundary: Bool) -> ParsedDialogs {
var notificationSettings: [PeerId: PeerNotificationSettings] = [:]
var readStates: [PeerId: [MessageId.Namespace: PeerReadState]] = [:]
var mentionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary] = [:]
var reactionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary] = [:]
var channelStates: [PeerId: Int32] = [:]
var topMessageIds: [PeerId: MessageId] = [:]
var ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout] = [:]
var viewForumAsMessages: [PeerId: Bool] = [:]
var storeMessages: [StoreMessage] = []
var nonPinnedDialogsTopMessageIds = Set<MessageId>()
var referencedFolders: [PeerGroupId: PeerGroupUnreadCountersSummary] = [:]
var itemIds: [PeerId] = []
let peers = AccumulatedPeers(chats: apiChats, users: apiUsers)
for dialog in apiDialogs {
let apiPeer: Api.Peer
let apiReadInboxMaxId: Int32
let apiReadOutboxMaxId: Int32
let apiTopMessage: Int32
let apiUnreadCount: Int32
let apiMarkedUnread: Bool
let apiUnreadMentionsCount: Int32
let apiUnreadReactionsCount: Int32
var apiChannelPts: Int32?
let apiNotificationSettings: Api.PeerNotifySettings
switch dialog {
case let .dialog(flags, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, peerNotificationSettings, pts, _, _, ttlPeriod):
if let peer = peers.get(peer.peerId) {
var isExluded = false
if let group = peer as? TelegramGroup {
if group.flags.contains(.deactivated) {
isExluded = true
}
}
if !isExluded {
itemIds.append(peer.id)
}
}
apiPeer = peer
apiTopMessage = topMessage
apiReadInboxMaxId = readInboxMaxId
apiReadOutboxMaxId = readOutboxMaxId
apiUnreadCount = unreadCount
apiMarkedUnread = (flags & (1 << 3)) != 0
apiUnreadMentionsCount = unreadMentionsCount
apiUnreadReactionsCount = unreadReactionsCount
apiNotificationSettings = peerNotificationSettings
apiChannelPts = pts
viewForumAsMessages[peer.peerId] = (flags & (1 << 6)) != 0
ttlPeriods[peer.peerId] = .known(ttlPeriod.flatMap(CachedPeerAutoremoveTimeout.Value.init(peerValue:)))
let isPinned = (flags & (1 << 2)) != 0
if !isPinned {
nonPinnedDialogsTopMessageIds.insert(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: topMessage))
}
let peerId: PeerId
switch apiPeer {
case let .peerUser(userId):
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
case let .peerChat(chatId):
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))
case let .peerChannel(channelId):
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))
}
if readStates[peerId] == nil {
readStates[peerId] = [:]
}
readStates[peerId]![Namespaces.Message.Cloud] = .idBased(maxIncomingReadId: apiReadInboxMaxId, maxOutgoingReadId: apiReadOutboxMaxId, maxKnownId: apiTopMessage, count: apiUnreadCount, markedUnread: apiMarkedUnread)
if apiTopMessage != 0 {
mentionTagSummaries[peerId] = MessageHistoryTagNamespaceSummary(version: 1, count: apiUnreadMentionsCount, range: MessageHistoryTagNamespaceCountValidityRange(maxId: apiTopMessage))
reactionTagSummaries[peerId] = MessageHistoryTagNamespaceSummary(version: 1, count: apiUnreadReactionsCount, range: MessageHistoryTagNamespaceCountValidityRange(maxId: apiTopMessage))
topMessageIds[peerId] = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: apiTopMessage)
}
if let apiChannelPts = apiChannelPts {
channelStates[peerId] = apiChannelPts
}
notificationSettings[peerId] = TelegramPeerNotificationSettings(apiSettings: apiNotificationSettings)
case let .dialogFolder(_, folder, _, _, unreadMutedPeersCount, _, unreadMutedMessagesCount, _):
switch folder {
case let .folder(_, id, _, _):
referencedFolders[PeerGroupId(rawValue: id)] = PeerGroupUnreadCountersSummary(all: PeerGroupUnreadCounters(messageCount: unreadMutedMessagesCount, chatCount: unreadMutedPeersCount))
}
}
}
var lowerNonPinnedIndex: MessageIndex?
for message in apiMessages {
var peerIsForum = false
if let peerId = message.peerId, let peer = peers.get(peerId), peer.isForumOrMonoForum {
peerIsForum = true
}
if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) {
var updatedStoreMessage = storeMessage
if case let .Id(id) = storeMessage.id {
if let channelPts = channelStates[id.peerId] {
var updatedAttributes = storeMessage.attributes
updatedAttributes.append(ChannelMessageStateVersionAttribute(pts: channelPts))
updatedStoreMessage = updatedStoreMessage.withUpdatedAttributes(updatedAttributes)
}
if !apiIsAtLowestBoundary, nonPinnedDialogsTopMessageIds.contains(id) {
let index = MessageIndex(id: id, timestamp: storeMessage.timestamp)
if lowerNonPinnedIndex == nil || lowerNonPinnedIndex! > index {
lowerNonPinnedIndex = index
}
}
}
storeMessages.append(updatedStoreMessage)
}
}
return ParsedDialogs(
itemIds: itemIds,
peers: peers,
notificationSettings: notificationSettings,
readStates: readStates,
mentionTagSummaries: mentionTagSummaries,
reactionTagSummaries: reactionTagSummaries,
channelStates: channelStates,
topMessageIds: topMessageIds,
storeMessages: storeMessages,
ttlPeriods: ttlPeriods,
viewForumAsMessages: viewForumAsMessages,
lowerNonPinnedIndex: lowerNonPinnedIndex,
referencedFolders: referencedFolders
)
}
struct FetchedChatList {
var chatPeerIds: [PeerId]
var peers: AccumulatedPeers
var notificationSettings: [PeerId: PeerNotificationSettings]
var ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout]
var viewForumAsMessages: [PeerId: Bool]
var readStates: [PeerId: [MessageId.Namespace: PeerReadState]]
var mentionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary]
var reactionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary]
var channelStates: [PeerId: Int32]
var storeMessages: [StoreMessage]
var topMessageIds: [PeerId: MessageId]
var lowerNonPinnedIndex: MessageIndex?
var pinnedItemIds: [PeerId]?
var folderSummaries: [PeerGroupId: PeerGroupUnreadCountersSummary]
var peerGroupIds: [PeerId: PeerGroupId]
var threadInfos: [PeerAndBoundThreadId: StoreMessageHistoryThreadData]
}
func fetchChatList(accountPeerId: PeerId, postbox: Postbox, network: Network, location: FetchChatListLocation, upperBound: MessageIndex, hash: Int64, limit: Int32) -> Signal<FetchedChatList?, NoError> {
return postbox.stateView()
|> mapToSignal { view -> Signal<AuthorizedAccountState, NoError> in
if let state = view.state as? AuthorizedAccountState {
return .single(state)
} else {
return .complete()
}
}
|> take(1)
|> mapToSignal { _ -> Signal<FetchedChatList?, NoError> in
let offset: Signal<(Int32, Int32, Api.InputPeer), NoError>
if upperBound.id.peerId.namespace == Namespaces.Peer.Empty {
offset = single((0, 0, Api.InputPeer.inputPeerEmpty), NoError.self)
} else {
offset = postbox.loadedPeerWithId(upperBound.id.peerId)
|> take(1)
|> map { peer in
return (upperBound.timestamp, upperBound.id.id, apiInputPeer(peer) ?? .inputPeerEmpty)
}
}
return offset
|> mapToSignal { (timestamp, id, peer) -> Signal<FetchedChatList?, NoError> in
let additionalPinnedChats: Signal<Api.messages.PeerDialogs?, NoError>
if case .inputPeerEmpty = peer, timestamp == 0 {
let folderId: Int32
switch location {
case .general:
folderId = 0
case let .group(groupId):
folderId = groupId.rawValue
}
additionalPinnedChats = network.request(Api.functions.messages.getPinnedDialogs(folderId: folderId))
|> retryRequestIfNotFrozen
} else {
additionalPinnedChats = .single(nil)
}
var flags: Int32 = 1 << 1
let requestFolderId: Int32
switch location {
case .general:
requestFolderId = 0
case let .group(groupId):
flags |= 1 << 0
requestFolderId = groupId.rawValue
}
let requestChats = network.request(Api.functions.messages.getDialogs(flags: flags, folderId: requestFolderId, offsetDate: timestamp, offsetId: id, offsetPeer: peer, limit: limit, hash: hash))
|> retryRequest
return combineLatest(requestChats, additionalPinnedChats)
|> mapToSignal { remoteChats, pinnedChats -> Signal<FetchedChatList?, NoError> in
if case .dialogsNotModified = remoteChats {
return .single(nil)
}
let extractedRemoteDialogs = extractDialogsData(dialogs: remoteChats)
let parsedRemoteChats = parseDialogs(accountPeerId: accountPeerId, apiDialogs: extractedRemoteDialogs.apiDialogs, apiMessages: extractedRemoteDialogs.apiMessages, apiChats: extractedRemoteDialogs.apiChats, apiUsers: extractedRemoteDialogs.apiUsers, apiIsAtLowestBoundary: extractedRemoteDialogs.apiIsAtLowestBoundary)
var parsedPinnedChats: ParsedDialogs?
if let pinnedChats = pinnedChats {
let extractedPinnedChats = extractDialogsData(peerDialogs: pinnedChats)
parsedPinnedChats = parseDialogs(accountPeerId: accountPeerId, apiDialogs: extractedPinnedChats.apiDialogs, apiMessages: extractedPinnedChats.apiMessages, apiChats: extractedPinnedChats.apiChats, apiUsers: extractedPinnedChats.apiUsers, apiIsAtLowestBoundary: extractedPinnedChats.apiIsAtLowestBoundary)
}
var combinedReferencedFolders = Set<PeerGroupId>()
combinedReferencedFolders.formUnion(parsedRemoteChats.referencedFolders.keys)
if let parsedPinnedChats = parsedPinnedChats {
combinedReferencedFolders.formUnion(Set(parsedPinnedChats.referencedFolders.keys))
}
var folderSignals: [Signal<(PeerGroupId, ParsedDialogs), NoError>] = []
if case .general = location {
for groupId in combinedReferencedFolders {
let flags: Int32 = 1 << 1
let requestFeed = network.request(Api.functions.messages.getDialogs(flags: flags, folderId: groupId.rawValue, offsetDate: 0, offsetId: 0, offsetPeer: .inputPeerEmpty, limit: 32, hash: 0))
|> retryRequest
|> map { result -> (PeerGroupId, ParsedDialogs) in
let extractedData = extractDialogsData(dialogs: result)
let parsedChats = parseDialogs(accountPeerId: accountPeerId, apiDialogs: extractedData.apiDialogs, apiMessages: extractedData.apiMessages, apiChats: extractedData.apiChats, apiUsers: extractedData.apiUsers, apiIsAtLowestBoundary: extractedData.apiIsAtLowestBoundary)
return (groupId, parsedChats)
}
folderSignals.append(requestFeed)
}
}
return combineLatest(folderSignals)
|> mapToSignal { folders -> Signal<FetchedChatList?, NoError> in
var peers = AccumulatedPeers()
var notificationSettings: [PeerId: PeerNotificationSettings] = [:]
var ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout] = [:]
var viewForumAsMessages: [PeerId: Bool] = [:]
var readStates: [PeerId: [MessageId.Namespace: PeerReadState]] = [:]
var mentionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary] = [:]
var reactionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary] = [:]
var channelStates: [PeerId: Int32] = [:]
var storeMessages: [StoreMessage] = []
var topMessageIds: [PeerId: MessageId] = [:]
peers = peers.union(with: parsedRemoteChats.peers)
notificationSettings.merge(parsedRemoteChats.notificationSettings, uniquingKeysWith: { _, updated in updated })
ttlPeriods.merge(parsedRemoteChats.ttlPeriods, uniquingKeysWith: { _, updated in updated })
viewForumAsMessages.merge(parsedRemoteChats.viewForumAsMessages, uniquingKeysWith: { _, updated in updated })
readStates.merge(parsedRemoteChats.readStates, uniquingKeysWith: { _, updated in updated })
mentionTagSummaries.merge(parsedRemoteChats.mentionTagSummaries, uniquingKeysWith: { _, updated in updated })
reactionTagSummaries.merge(parsedRemoteChats.reactionTagSummaries, uniquingKeysWith: { _, updated in updated })
channelStates.merge(parsedRemoteChats.channelStates, uniquingKeysWith: { _, updated in updated })
storeMessages.append(contentsOf: parsedRemoteChats.storeMessages)
topMessageIds.merge(parsedRemoteChats.topMessageIds, uniquingKeysWith: { _, updated in updated })
if let parsedPinnedChats = parsedPinnedChats {
peers = peers.union(with: parsedPinnedChats.peers)
notificationSettings.merge(parsedPinnedChats.notificationSettings, uniquingKeysWith: { _, updated in updated })
ttlPeriods.merge(parsedPinnedChats.ttlPeriods, uniquingKeysWith: { _, updated in updated })
viewForumAsMessages.merge(parsedPinnedChats.viewForumAsMessages, uniquingKeysWith: { _, updated in updated })
readStates.merge(parsedPinnedChats.readStates, uniquingKeysWith: { _, updated in updated })
mentionTagSummaries.merge(parsedPinnedChats.mentionTagSummaries, uniquingKeysWith: { _, updated in updated })
reactionTagSummaries.merge(parsedPinnedChats.reactionTagSummaries, uniquingKeysWith: { _, updated in updated })
channelStates.merge(parsedPinnedChats.channelStates, uniquingKeysWith: { _, updated in updated })
storeMessages.append(contentsOf: parsedPinnedChats.storeMessages)
topMessageIds.merge(parsedPinnedChats.topMessageIds, uniquingKeysWith: { _, updated in updated })
}
var peerGroupIds: [PeerId: PeerGroupId] = [:]
if case let .group(groupId) = location {
for peerId in parsedRemoteChats.itemIds {
peerGroupIds[peerId] = groupId
}
}
for (groupId, folderChats) in folders {
for peerId in folderChats.itemIds {
peerGroupIds[peerId] = groupId
}
peers = peers.union(with: folderChats.peers)
notificationSettings.merge(folderChats.notificationSettings, uniquingKeysWith: { _, updated in updated })
ttlPeriods.merge(folderChats.ttlPeriods, uniquingKeysWith: { _, updated in updated })
viewForumAsMessages.merge(folderChats.viewForumAsMessages, uniquingKeysWith: { _, updated in updated })
readStates.merge(folderChats.readStates, uniquingKeysWith: { _, updated in updated })
mentionTagSummaries.merge(folderChats.mentionTagSummaries, uniquingKeysWith: { _, updated in updated })
reactionTagSummaries.merge(folderChats.reactionTagSummaries, uniquingKeysWith: { _, updated in updated })
channelStates.merge(folderChats.channelStates, uniquingKeysWith: { _, updated in updated })
storeMessages.append(contentsOf: folderChats.storeMessages)
}
var pinnedItemIds: [PeerId]?
if let parsedPinnedChats = parsedPinnedChats {
var array: [PeerId] = []
for peerId in parsedPinnedChats.itemIds {
if case let .group(groupId) = location {
peerGroupIds[peerId] = groupId
}
array.append(peerId)
}
pinnedItemIds = array
}
var folderSummaries: [PeerGroupId: PeerGroupUnreadCountersSummary] = [:]
for (groupId, summary) in parsedRemoteChats.referencedFolders {
folderSummaries[groupId] = summary
}
if let parsedPinnedChats = parsedPinnedChats {
for (groupId, summary) in parsedPinnedChats.referencedFolders {
folderSummaries[groupId] = summary
}
}
let result: FetchedChatList? = FetchedChatList(
chatPeerIds: parsedRemoteChats.itemIds + (pinnedItemIds ?? []),
peers: peers,
notificationSettings: notificationSettings,
ttlPeriods: ttlPeriods,
viewForumAsMessages: viewForumAsMessages,
readStates: readStates,
mentionTagSummaries: mentionTagSummaries,
reactionTagSummaries: reactionTagSummaries,
channelStates: channelStates,
storeMessages: storeMessages,
topMessageIds: topMessageIds,
lowerNonPinnedIndex: parsedRemoteChats.lowerNonPinnedIndex,
pinnedItemIds: pinnedItemIds,
folderSummaries: folderSummaries,
peerGroupIds: peerGroupIds,
threadInfos: [:]
)
return resolveUnknownEmojiFiles(postbox: postbox, source: .network(network), messages: storeMessages, reactions: [], result: result)
|> mapToSignal { result in
if let result = result {
return resolveForumThreads(accountPeerId: accountPeerId, postbox: postbox, source: .network(network), fetchedChatList: result)
|> map(Optional.init)
} else {
return .single(result)
}
}
}
}
}
}
}
@@ -0,0 +1,30 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
func fetchSecretFileResource(
accountPeerId: PeerId,
postbox: Postbox,
network: Network,
mediaReferenceRevalidationContext: MediaReferenceRevalidationContext,
networkStatsContext: NetworkStatsContext,
resource: SecretFileMediaResource,
intervals: Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError>,
parameters: MediaResourceFetchParameters?
) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
return multipartFetch(
accountPeerId: accountPeerId,
postbox: postbox,
network: network,
mediaReferenceRevalidationContext: mediaReferenceRevalidationContext,
networkStatsContext: networkStatsContext,
resource: resource,
datacenterId: resource.datacenterId,
size: resource.size,
intervals: intervals,
parameters: parameters,
encryptionKey: resource.key,
decryptedSize: resource.decryptedSize
)
}
@@ -0,0 +1,16 @@
import Foundation
import Postbox
enum InternalAccountState {
static func addMessages(transaction: Transaction, messages: [StoreMessage], location: AddMessagesLocation) -> [Int64 : MessageId] {
return transaction.addMessages(messages, location: location)
}
static func deleteMessages(transaction: Transaction, ids: [MessageId], forEachMedia: ((Media) -> Void)?) {
transaction.deleteMessages(ids, forEachMedia: forEachMedia)
}
static func invalidateChannelState(peerId: PeerId) {
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
import Foundation
import SwiftSignalKit
import Postbox
func initializedAppSettingsAfterLogin(transaction: Transaction, appVersion: String, syncContacts: Bool) {
updateAppChangelogState(transaction: transaction, { state in
var state = state
state.checkedVersion = appVersion
state.previousVersion = appVersion
return state
})
transaction.updatePreferencesEntry(key: PreferencesKeys.contactsSettings, { _ in
return PreferencesEntry(ContactsSettings(synchronizeContacts: syncContacts))
})
}
@@ -0,0 +1,98 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
private typealias SignalKitTimer = SwiftSignalKit.Timer
private final class AccountPresenceManagerImpl {
private let queue: Queue
private let network: Network
let isPerformingUpdate = ValuePromise<Bool>(false, ignoreRepeated: true)
private var shouldKeepOnlinePresenceDisposable: Disposable?
private let currentRequestDisposable = MetaDisposable()
private var onlineTimer: SignalKitTimer?
private var wasOnline: Bool = false
init(queue: Queue, shouldKeepOnlinePresence: Signal<Bool, NoError>, network: Network) {
self.queue = queue
self.network = network
self.shouldKeepOnlinePresenceDisposable = (shouldKeepOnlinePresence
|> distinctUntilChanged
|> deliverOn(self.queue)).start(next: { [weak self] value in
guard let `self` = self else {
return
}
if self.wasOnline != value {
self.wasOnline = value
self.updatePresence(value)
}
})
}
deinit {
assert(self.queue.isCurrent())
self.shouldKeepOnlinePresenceDisposable?.dispose()
self.currentRequestDisposable.dispose()
self.onlineTimer?.invalidate()
}
private func updatePresence(_ isOnline: Bool) {
let request: Signal<Api.Bool, MTRpcError>
if isOnline {
let timer = SignalKitTimer(timeout: 30.0, repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updatePresence(true)
}, queue: self.queue)
self.onlineTimer = timer
timer.start()
request = self.network.request(Api.functions.account.updateStatus(offline: .boolFalse))
} else {
self.onlineTimer?.invalidate()
self.onlineTimer = nil
request = self.network.request(Api.functions.account.updateStatus(offline: .boolTrue))
}
self.isPerformingUpdate.set(true)
self.currentRequestDisposable.set((request
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> deliverOn(self.queue)).start(completed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isPerformingUpdate.set(false)
}))
}
}
final class AccountPresenceManager {
private let queue = Queue()
private let impl: QueueLocalObject<AccountPresenceManagerImpl>
init(shouldKeepOnlinePresence: Signal<Bool, NoError>, network: Network) {
let queue = self.queue
self.impl = QueueLocalObject(queue: self.queue, generate: {
return AccountPresenceManagerImpl(queue: queue, shouldKeepOnlinePresence: shouldKeepOnlinePresence, network: network)
})
}
func isPerformingUpdate() -> Signal<Bool, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.isPerformingUpdate.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
}
@@ -0,0 +1,31 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func managedAnimatedEmojiUpdates(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = _internal_loadedStickerPack(postbox: postbox, network: network, reference: .animatedEmoji, forceActualized: true)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedAnimatedEmojiAnimationsUpdates(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = _internal_loadedStickerPack(postbox: postbox, network: network, reference: .animatedEmojiAnimations, forceActualized: true)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedGenericEmojiEffects(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = _internal_loadedStickerPack(postbox: postbox, network: network, reference: .emojiGenericAnimations, forceActualized: true)
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
@@ -0,0 +1,55 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func updateAppConfigurationOnce(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Int32 in
return currentAppConfiguration(transaction: transaction).hash
}
|> mapToSignal { hash -> Signal<Void, NoError> in
return network.request(Api.functions.help.getAppConfig(hash: hash))
|> map { result -> (data: Api.JSONValue, hash: Int32)? in
switch result {
case let .appConfig(updatedHash, config):
return (config, updatedHash)
case .appConfigNotModified:
return nil
}
}
|> `catch` { _ -> Signal<(data: Api.JSONValue, hash: Int32)?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
guard let result = result else {
return .complete()
}
return postbox.transaction { transaction -> Void in
if let data = JSON(apiJson: result.data) {
updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in
var configuration = configuration
configuration.data = data
configuration.hash = result.hash
return configuration
})
if let audioTranscriptionCooldownUntilTimestamp = data["transcribe_audio_trial_cooldown_until"] as? Double {
_internal_updateAudioTranscriptionTrialState(transaction: transaction, { $0.withUpdatedCooldownUntilTime(Int32(audioTranscriptionCooldownUntilTimestamp)) })
} else {
_internal_updateAudioTranscriptionTrialState(transaction: transaction, { $0.withUpdatedCooldownUntilTime(nil) })
}
}
}
}
}
}
func managedAppConfigurationUpdates(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = Signal<Void, NoError> { subscriber in
return updateAppConfigurationOnce(postbox: postbox, network: network).start(completed: {
subscriber.putCompletion()
})
}
return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
@@ -0,0 +1,49 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func managedAutodownloadSettingsUpdates(accountManager: AccountManager<TelegramAccountManagerTypes>, network: Network) -> Signal<Void, NoError> {
let poll = Signal<Void, NoError> { subscriber in
return (network.request(Api.functions.account.getAutoDownloadSettings())
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<Void, NoError> in
guard let result else {
return .complete()
}
return updateAutodownloadSettingsInteractively(accountManager: accountManager, { _ -> AutodownloadSettings in
return AutodownloadSettings(apiAutodownloadSettings: result)
})
}).start(completed: {
subscriber.putCompletion()
})
}
return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
public enum SavedAutodownloadPreset {
case low
case medium
case high
}
public func saveAutodownloadSettings(account: Account, preset: SavedAutodownloadPreset, settings: AutodownloadPresetSettings) -> Signal<Void, NoError> {
var flags: Int32 = 0
switch preset {
case .low:
flags |= (1 << 0)
case .high:
flags |= (1 << 1)
default:
break
}
return account.network.request(Api.functions.account.saveAutoDownloadSettings(flags: flags, settings: apiAutodownloadPresetSettings(settings)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
@@ -0,0 +1,201 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private typealias SignalKitTimer = SwiftSignalKit.Timer
private final class ManagedAutoremoveMessageOperationsHelper {
var entry: (TimestampBasedMessageAttributesEntry, MetaDisposable)?
func update(_ head: TimestampBasedMessageAttributesEntry?) -> (disposeOperations: [Disposable], beginOperations: [(TimestampBasedMessageAttributesEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(TimestampBasedMessageAttributesEntry, MetaDisposable)] = []
if self.entry?.0.index != head?.index {
if let (_, disposable) = self.entry {
self.entry = nil
disposeOperations.append(disposable)
}
if let head = head {
let disposable = MetaDisposable()
self.entry = (head, disposable)
beginOperations.append((head, disposable))
}
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
if let entry = entry {
return [entry.1]
} else {
return []
}
}
}
func managedAutoremoveMessageOperations(network: Network, postbox: Postbox, isRemove: Bool) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic(value: ManagedAutoremoveMessageOperationsHelper())
let timeOffsetOnce = Signal<Double, NoError> { subscriber in
subscriber.putNext(network.globalTimeDifference)
return EmptyDisposable
}
let timeOffset = (
timeOffsetOnce
|> then(
Signal<Double, NoError>.complete()
|> delay(1.0, queue: .mainQueue())
)
)
|> restart
|> map { value -> Double in
round(value)
}
|> distinctUntilChanged
Logger.shared.log("Autoremove", "starting isRemove: \(isRemove)")
let tag: UInt16 = isRemove ? 0 : 1
let disposable = combineLatest(timeOffset, postbox.timestampBasedMessageAttributesView(tag: tag)).start(next: { timeOffset, view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(TimestampBasedMessageAttributesEntry, MetaDisposable)]) in
return helper.update(view.head)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + timeOffset
let delay = max(0.0, Double(entry.timestamp) - timestamp)
Logger.shared.log("Autoremove", "Scheduling autoremove for \(entry.messageId) at \(entry.timestamp) (in \(delay) seconds)")
let signal = Signal<Void, NoError>.complete()
|> suspendAwareDelay(delay, queue: Queue.concurrentDefaultQueue())
|> then(postbox.transaction { transaction -> Void in
Logger.shared.log("Autoremove", "Performing autoremove for \(entry.messageId), isRemove: \(isRemove)")
if let message = transaction.getMessage(entry.messageId) {
if message.id.peerId.namespace == Namespaces.Peer.SecretChat || isRemove {
_internal_deleteMessages(transaction: transaction, mediaBox: postbox.mediaBox, ids: [entry.messageId])
} else {
transaction.updateMessage(message.id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var updatedMedia = currentMessage.media
for i in 0 ..< updatedMedia.count {
if let _ = updatedMedia[i] as? TelegramMediaImage {
updatedMedia[i] = TelegramMediaExpiredContent(data: .image)
} else if let file = updatedMedia[i] as? TelegramMediaFile {
if file.isInstantVideo {
updatedMedia[i] = TelegramMediaExpiredContent(data: .videoMessage)
} else if file.isVoice {
updatedMedia[i] = TelegramMediaExpiredContent(data: .voiceMessage)
} else {
updatedMedia[i] = TelegramMediaExpiredContent(data: .file)
}
}
}
var updatedAttributes = currentMessage.attributes
for i in 0 ..< updatedAttributes.count {
if let _ = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute {
updatedAttributes.remove(at: i)
break
}
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: updatedAttributes, media: updatedMedia))
})
}
} else {
transaction.clearTimestampBasedAttribute(id: entry.messageId, tag: tag)
Logger.shared.log("Autoremove", "No message to autoremove for \(entry.messageId)")
}
})
disposable.set(signal.start())
}
})
return ActionDisposable {
disposable.dispose()
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
}
}
}
func managedAutoexpireStoryOperations(network: Network, postbox: Postbox) -> Signal<Void, NoError> {
return Signal { _ in
let timeOffsetOnce = Signal<Double, NoError> { subscriber in
subscriber.putNext(network.globalTimeDifference)
return EmptyDisposable
}
let timeOffset = (
timeOffsetOnce
|> then(
Signal<Double, NoError>.complete()
|> delay(1.0, queue: .mainQueue())
)
)
|> restart
|> map { value -> Double in
round(value)
}
|> distinctUntilChanged
Logger.shared.log("Autoexpire stories", "starting")
let currentDisposable = MetaDisposable()
let disposable = combineLatest(timeOffset, postbox.combinedView(keys: [PostboxViewKey.storyExpirationTimeItems])).start(next: { timeOffset, views in
guard let view = views.views[PostboxViewKey.storyExpirationTimeItems] as? StoryExpirationTimeItemsView, let topItem = view.topEntry else {
currentDisposable.set(nil)
return
}
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + timeOffset
let delay = max(0.0, Double(topItem.expirationTimestamp) - timestamp)
let signal = Signal<Void, NoError>.complete()
|> suspendAwareDelay(delay, queue: Queue.concurrentDefaultQueue())
|> then(postbox.transaction { transaction -> Void in
var idsByPeerId: [PeerId: [Int32]] = [:]
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + timeOffset)
for id in transaction.getExpiredStoryIds(belowTimestamp: timestamp + 3) {
if idsByPeerId[id.peerId] == nil {
idsByPeerId[id.peerId] = [id.id]
} else {
idsByPeerId[id.peerId]?.append(id.id)
}
}
for (peerId, ids) in idsByPeerId {
var items = transaction.getStoryItems(peerId: peerId)
items.removeAll(where: { ids.contains($0.id) })
transaction.setStoryItems(peerId: peerId, items: items)
}
})
currentDisposable.set(signal.start())
})
return ActionDisposable {
disposable.dispose()
currentDisposable.dispose()
}
}
}
@@ -0,0 +1,166 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
private final class ManagedChatListHolesState {
private var currentHole: (ChatListHolesEntry, Disposable)?
func clearDisposables() -> [Disposable] {
if let (_, disposable) = self.currentHole {
self.currentHole = nil
return [disposable]
} else {
return []
}
}
func update(entries: [ChatListHolesEntry]) -> (removed: [Disposable], added: [ChatListHolesEntry: MetaDisposable]) {
var removed: [Disposable] = []
var added: [ChatListHolesEntry: MetaDisposable] = [:]
if let (entry, disposable) = self.currentHole {
if !entries.contains(entry) {
removed.append(disposable)
self.currentHole = nil
}
}
if self.currentHole == nil, let entry = entries.first {
let disposable = MetaDisposable()
self.currentHole = (entry, disposable)
added[entry] = disposable
}
return (removed, added)
}
}
func managedChatListHoles(network: Network, postbox: Postbox, accountPeerId: PeerId) -> Signal<Void, NoError> {
return Signal { _ in
let state = Atomic(value: ManagedChatListHolesState())
let topRootHoleKey: PostboxViewKey = .allChatListHoles(.root)
let topArchiveHoleKey: PostboxViewKey = .allChatListHoles(Namespaces.PeerGroup.archive)
let filtersKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.chatListFilters]))
let combinedView = postbox.combinedView(keys: [topRootHoleKey, topArchiveHoleKey, filtersKey])
let disposable = combineLatest(postbox.chatListHolesView(), combinedView).start(next: { view, combinedView in
var entries = Array(view.entries).sorted(by: { lhs, rhs in
return lhs.hole.index > rhs.hole.index
})
if let preferencesView = combinedView.views[filtersKey] as? PreferencesView, let filtersState = preferencesView.values[PreferencesKeys.chatListFilters]?.get(ChatListFiltersState.self), !filtersState.filters.isEmpty {
if let topRootHole = combinedView.views[topRootHoleKey] as? AllChatListHolesView, let hole = topRootHole.latestHole {
let entry = ChatListHolesEntry(groupId: .root, hole: hole)
if !entries.contains(entry) {
entries.append(entry)
}
}
if let topArchiveHole = combinedView.views[topArchiveHoleKey] as? AllChatListHolesView, let hole = topArchiveHole.latestHole {
if !view.entries.contains(ChatListHolesEntry(groupId: Namespaces.PeerGroup.archive, hole: hole)) {
let entry = ChatListHolesEntry(groupId: Namespaces.PeerGroup.archive, hole: hole)
if !entries.contains(entry) {
entries.append(entry)
}
}
}
}
let (removed, added) = state.with { state in
return state.update(entries: entries)
}
for disposable in removed {
disposable.dispose()
}
for (entry, disposable) in added {
disposable.set(fetchChatListHole(postbox: postbox, network: network, accountPeerId: accountPeerId, groupId: entry.groupId, hole: entry.hole).start())
}
})
return ActionDisposable {
disposable.dispose()
for disposable in state.with({ state -> [Disposable] in
state.clearDisposables()
}) {
disposable.dispose()
}
}
}
}
private final class ManagedForumTopicListHolesState {
private var currentHoles: [ForumTopicListHolesEntry: Disposable] = [:]
func clearDisposables() -> [Disposable] {
let disposables = Array(self.currentHoles.values)
self.currentHoles.removeAll()
return disposables
}
func update(entries: [ForumTopicListHolesEntry]) -> (removed: [Disposable], added: [ForumTopicListHolesEntry: MetaDisposable]) {
var removed: [Disposable] = []
var added: [ForumTopicListHolesEntry: MetaDisposable] = [:]
for entry in entries {
if self.currentHoles[entry] == nil {
let disposable = MetaDisposable()
added[entry] = disposable
self.currentHoles[entry] = disposable
}
}
var removedKeys: [ForumTopicListHolesEntry] = []
for (entry, disposable) in self.currentHoles {
if !entries.contains(entry) {
removed.append(disposable)
removedKeys.append(entry)
}
}
for key in removedKeys {
self.currentHoles.removeValue(forKey: key)
}
return (removed, added)
}
}
func managedForumTopicListHoles(network: Network, postbox: Postbox, accountPeerId: PeerId) -> Signal<Void, NoError> {
return Signal { _ in
let state = Atomic(value: ManagedForumTopicListHolesState())
let disposable = postbox.forumTopicListHolesView().start(next: { view in
let entries = Array(view.entries)
let (removed, added) = state.with { state in
return state.update(entries: entries)
}
for disposable in removed {
disposable.dispose()
}
for (entry, disposable) in added {
disposable.set((_internal_requestMessageHistoryThreads(accountPeerId: accountPeerId, postbox: postbox, network: network, peerId: entry.peerId, query: nil, offsetIndex: entry.index, limit: 100)
|> mapToSignal { result -> Signal<Never, LoadMessageHistoryThreadsError> in
return postbox.transaction { transaction in
return applyLoadMessageHistoryThreadsResults(accountPeerId: accountPeerId, transaction: transaction, results: [result])
}
|> castError(LoadMessageHistoryThreadsError.self)
|> ignoreValues
}).start())
}
})
return ActionDisposable {
disposable.dispose()
for disposable in state.with({ state -> [Disposable] in
state.clearDisposables()
}) {
disposable.dispose()
}
}
}
}
@@ -0,0 +1,575 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedCloudChatRemoveMessagesOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, (entry.contents is CloudChatRemoveMessagesOperation || entry.contents is CloudChatRemoveChatOperation || entry.contents is CloudChatClearHistoryOperation) {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedCloudChatRemoveMessagesOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedCloudChatRemoveMessagesOperationsHelper>(value: ManagedCloudChatRemoveMessagesOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: OperationLogTags.CloudChatRemoveMessages, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? CloudChatRemoveMessagesOperation {
if let peer = transaction.getPeer(entry.peerId) {
return removeMessages(postbox: postbox, network: network, stateManager: stateManager, peer: peer, operation: operation)
} else {
return .complete()
}
} else if let operation = entry.contents as? CloudChatRemoveChatOperation {
if let peer = transaction.getPeer(entry.peerId) {
return removeChat(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, peer: peer, operation: operation)
} else {
return .complete()
}
} else if let operation = entry.contents as? CloudChatClearHistoryOperation {
if let peer = transaction.getPeer(entry.peerId) {
return _internal_clearHistory(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, peer: peer, operation: operation)
} else {
return .complete()
}
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func removeMessages(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatRemoveMessagesOperation) -> Signal<Void, NoError> {
var isScheduled = false
var isQuickReply = false
for id in operation.messageIds {
if id.namespace == Namespaces.Message.ScheduledCloud {
isScheduled = true
break
} else if id.namespace == Namespaces.Message.QuickReplyCloud {
isQuickReply = true
break
}
}
if isScheduled {
if let inputPeer = apiInputPeer(peer) {
var signal: Signal<Void, NoError> = .complete()
for s in stride(from: 0, to: operation.messageIds.count, by: 100) {
let ids = Array(operation.messageIds[s ..< min(s + 100, operation.messageIds.count)])
let partSignal = network.request(Api.functions.messages.deleteScheduledMessages(peer: inputPeer, id: ids.map { $0.id }))
|> map { result -> Api.Updates? in
return result
}
|> `catch` { _ in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
stateManager.addUpdates(updates)
}
return .complete()
}
signal = signal
|> then(partSignal)
}
return signal
} else {
return .complete()
}
} else if isQuickReply {
if let threadId = operation.threadId {
var signal: Signal<Void, NoError> = .complete()
for s in stride(from: 0, to: operation.messageIds.count, by: 100) {
let ids = Array(operation.messageIds[s ..< min(s + 100, operation.messageIds.count)])
let partSignal = network.request(Api.functions.messages.deleteQuickReplyMessages(shortcutId: Int32(clamping: threadId), id: ids.map(\.id)))
|> map { result -> Api.Updates? in
return result
}
|> `catch` { _ in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
stateManager.addUpdates(updates)
}
return .complete()
}
signal = signal
|> then(partSignal)
}
return signal
} else {
return .complete()
}
} else if peer.id.namespace == Namespaces.Peer.CloudChannel {
if let inputChannel = apiInputChannel(peer) {
var signal: Signal<Void, NoError> = .complete()
for s in stride(from: 0, to: operation.messageIds.count, by: 100) {
let ids = Array(operation.messageIds[s ..< min(s + 100, operation.messageIds.count)])
let partSignal = network.request(Api.functions.channels.deleteMessages(channel: inputChannel, id: ids.map { $0.id }))
|> map { result -> Api.messages.AffectedMessages? in
return result
}
|> `catch` { _ in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .affectedMessages(pts, ptsCount):
stateManager.addUpdateGroups([.updateChannelPts(channelId: peer.id.id._internalGetInt64Value(), pts: pts, ptsCount: ptsCount)])
}
}
return .complete()
}
signal = signal
|> then(partSignal)
}
return signal
} else {
return .complete()
}
} else {
var flags: Int32
switch operation.type {
case .forEveryone:
flags = (1 << 0)
default:
flags = 0
}
var signal: Signal<Void, NoError> = .complete()
for s in stride(from: 0, to: operation.messageIds.count, by: 100) {
let ids = Array(operation.messageIds[s ..< min(s + 100, operation.messageIds.count)])
let partSignal = network.request(Api.functions.messages.deleteMessages(flags: flags, id: ids.map { $0.id }))
|> map { result -> Api.messages.AffectedMessages? in
return result
}
|> `catch` { _ in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .affectedMessages(pts, ptsCount):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
}
}
return .complete()
}
signal = signal
|> then(partSignal)
}
return signal
}
}
private func removeChat(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatRemoveChatOperation) -> Signal<Void, NoError> {
if peer.id.namespace == Namespaces.Peer.CloudChannel {
if let inputChannel = apiInputChannel(peer) {
let signal: Signal<Api.Updates, MTRpcError>
if operation.deleteGloballyIfPossible {
signal = network.request(Api.functions.channels.deleteChannel(channel: inputChannel))
|> `catch` { _ -> Signal<Api.Updates, MTRpcError> in
return network.request(Api.functions.channels.leaveChannel(channel: inputChannel))
}
} else {
signal = network.request(Api.functions.channels.leaveChannel(channel: inputChannel))
}
let reportSignal: Signal<Api.Bool, NoError>
if let inputPeer = apiInputPeer(peer), operation.reportChatSpam {
reportSignal = network.request(Api.functions.messages.reportSpam(peer: inputPeer))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
} else {
reportSignal = .single(.boolTrue)
}
return combineLatest(signal
|> map { result -> Api.Updates? in
return result
}
|> `catch` { _ in
return .single(nil)
}, reportSignal)
|> mapToSignal { updates, _ in
if let updates = updates {
stateManager.addUpdates(updates)
}
return .complete()
}
} else {
return .complete()
}
} else if peer.id.namespace == Namespaces.Peer.CloudGroup {
let deleteUser: Signal<Void, NoError>
if operation.deleteGloballyIfPossible {
deleteUser = network.request(Api.functions.messages.deleteChat(chatId: peer.id.id._internalGetInt64Value()))
|> `catch` { _ in
return .single(.boolFalse)
}
|> mapToSignal { _ in
return .complete()
}
} else {
deleteUser = network.request(Api.functions.messages.deleteChatUser(flags: 0, chatId: peer.id.id._internalGetInt64Value(), userId: Api.InputUser.inputUserSelf))
|> map { result -> Api.Updates? in
return result
}
|> `catch` { _ in
return .single(nil)
}
|> mapToSignal { updates in
if let updates = updates {
stateManager.addUpdates(updates)
}
return .complete()
}
}
let reportSignal: Signal<Void, NoError>
if let inputPeer = apiInputPeer(peer), operation.reportChatSpam {
reportSignal = network.request(Api.functions.messages.reportSpam(peer: inputPeer))
|> mapToSignal { _ -> Signal<Void, MTRpcError> in
return .complete()
}
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
reportSignal = .complete()
}
let deleteMessages: Signal<Void, NoError>
if let inputPeer = apiInputPeer(peer), let topMessageId = operation.topMessageId ?? transaction.getTopPeerMessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud) {
deleteMessages = requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: topMessageId.id, justClear: false, minTimestamp: nil, maxTimestamp: nil, type: operation.deleteGloballyIfPossible ? .forEveryone : .forLocalPeer)
} else {
deleteMessages = .complete()
}
return deleteMessages
|> then(deleteUser)
|> then(reportSignal)
|> then(postbox.transaction { transaction -> Void in
_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, threadId: nil, namespaces: .all)
})
} else if peer.id.namespace == Namespaces.Peer.CloudUser {
if let inputPeer = apiInputPeer(peer) {
let reportSignal: Signal<Void, NoError>
if let inputPeer = apiInputPeer(peer), operation.reportChatSpam {
reportSignal = network.request(Api.functions.messages.reportSpam(peer: inputPeer))
|> mapToSignal { _ -> Signal<Void, MTRpcError> in
return .complete()
}
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
reportSignal = .complete()
}
return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId?.id ?? Int32.max - 1, justClear: false, minTimestamp: nil, maxTimestamp: nil, type: operation.deleteGloballyIfPossible ? .forEveryone : .forLocalPeer)
|> then(reportSignal)
|> then(postbox.transaction { transaction -> Void in
_internal_clearHistory(transaction: transaction, mediaBox: postbox.mediaBox, peerId: peer.id, threadId: nil, namespaces: .not(Namespaces.Message.allNonRegular))
})
} else {
return .complete()
}
} else {
return .complete()
}
}
private func requestClearHistory(postbox: Postbox, network: Network, stateManager: AccountStateManager, inputPeer: Api.InputPeer, maxId: Int32, justClear: Bool, minTimestamp: Int32?, maxTimestamp: Int32?, type: CloudChatClearHistoryType) -> Signal<Void, NoError> {
var flags: Int32 = 0
if justClear {
flags |= 1 << 0
}
if case .forEveryone = type {
flags |= 1 << 1
}
var updatedMaxId = maxId
if minTimestamp != nil {
flags |= 1 << 2
updatedMaxId = 0
}
if maxTimestamp != nil {
flags |= 1 << 3
updatedMaxId = 0
}
let signal = network.request(Api.functions.messages.deleteHistory(flags: flags, peer: inputPeer, maxId: updatedMaxId, minDate: minTimestamp, maxDate: maxTimestamp))
|> map { result -> Api.messages.AffectedHistory? in
return result
}
|> `catch` { _ -> Signal<Api.messages.AffectedHistory?, Bool> in
return .fail(true)
}
|> mapToSignal { result -> Signal<Void, Bool> in
if let result = result {
switch result {
case let .affectedHistory(pts, ptsCount, offset):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
if offset == 0 {
return .fail(true)
} else {
return .complete()
}
}
} else {
return .fail(true)
}
}
return (signal |> restart)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
}
private func _internal_clearHistory(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatClearHistoryOperation) -> Signal<Void, NoError> {
if peer.id.namespace == Namespaces.Peer.CloudGroup || peer.id.namespace == Namespaces.Peer.CloudUser {
if case .quickReplyMessages = operation.type {
guard let threadId = operation.threadId else {
return .complete()
}
let signal = network.request(Api.functions.messages.deleteQuickReplyShortcut(shortcutId: Int32(clamping: threadId)))
|> map { result -> Api.Bool? in
return result
}
|> `catch` { _ -> Signal<Api.Bool?, Bool> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, Bool> in
return .fail(true)
}
return (signal |> restart)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
}
if let inputPeer = apiInputPeer(peer) {
if peer.id == stateManager.accountPeerId, let threadId = operation.threadId {
guard let inputSubPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) else {
return .complete()
}
var flags: Int32 = 0
var updatedMaxId = operation.topMessageId.id
if operation.minTimestamp != nil {
flags |= 1 << 2
updatedMaxId = 0
}
if operation.maxTimestamp != nil {
flags |= 1 << 3
updatedMaxId = 0
}
let signal = network.request(Api.functions.messages.deleteSavedHistory(flags: flags, parentPeer: nil, peer: inputSubPeer, maxId: updatedMaxId, minDate: operation.minTimestamp, maxDate: operation.maxTimestamp))
|> map { result -> Api.messages.AffectedHistory? in
return result
}
|> `catch` { _ -> Signal<Api.messages.AffectedHistory?, Bool> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, Bool> in
if let result = result {
switch result {
case let .affectedHistory(pts, ptsCount, offset):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
if offset == 0 {
return .fail(true)
} else {
return .complete()
}
}
} else {
return .fail(true)
}
}
return (signal |> restart)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId.id, justClear: true, minTimestamp: operation.minTimestamp, maxTimestamp: operation.maxTimestamp, type: operation.type)
}
} else {
return .complete()
}
} else if peer.id.namespace == Namespaces.Peer.CloudChannel, let inputChannel = apiInputChannel(peer) {
if operation.minTimestamp != nil {
return .complete()
} else {
if let threadId = operation.threadId {
if peer.isMonoForum {
guard let inputPeer = apiInputPeer(peer) else {
return .complete()
}
guard let inputSubPeer = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) else {
return .complete()
}
var flags: Int32 = 0
var updatedMaxId = operation.topMessageId.id
if operation.minTimestamp != nil {
flags |= 1 << 2
updatedMaxId = 0
}
if operation.maxTimestamp != nil {
flags |= 1 << 3
updatedMaxId = 0
}
flags |= 1 << 0
let signal = network.request(Api.functions.messages.deleteSavedHistory(flags: flags, parentPeer: inputPeer, peer: inputSubPeer, maxId: updatedMaxId, minDate: operation.minTimestamp, maxDate: operation.maxTimestamp))
|> map { result -> Api.messages.AffectedHistory? in
return result
}
|> `catch` { _ -> Signal<Api.messages.AffectedHistory?, Bool> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, Bool> in
if let result = result {
switch result {
case let .affectedHistory(pts, ptsCount, offset):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
if offset == 0 {
return .fail(true)
} else {
return .complete()
}
}
} else {
return .fail(true)
}
}
return (signal |> restart)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
guard let inputPeer = apiInputPeer(peer) else {
return .complete()
}
return network.request(Api.functions.messages.deleteTopicHistory(peer: inputPeer, topMsgId: Int32(clamping: threadId)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AffectedHistory?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let _ = result {
}
return .complete()
}
}
} else {
var flags: Int32 = 0
if operation.type == .forEveryone {
flags |= 1 << 0
}
return network.request(Api.functions.channels.deleteHistory(flags: flags, channel: inputChannel, maxId: operation.topMessageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
stateManager.addUpdates(updates)
}
return .complete()
}
}
}
} else {
assertionFailure()
return .complete()
}
}
@@ -0,0 +1,124 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func managedConfigurationUpdates(accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = Signal<Void, NoError> { subscriber in
return (combineLatest(
network.request(Api.functions.help.getConfig()) |> retryRequest,
network.request(Api.functions.messages.getDefaultHistoryTTL()) |> retryRequestIfNotFrozen
)
|> mapToSignal { result, defaultHistoryTtl -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Signal<Void, NoError> in
switch result {
case let .config(flags, _, _, _, _, dcOptions, _, chatSizeMax, megagroupSizeMax, forwardedCountMax, _, _, _, _, _, _, _, _, editTimeLimit, revokeTimeLimit, revokePmTimeLimit, _, stickersRecentLimit, _, _, _, _, _, _, _, autoupdateUrlPrefix, gifSearchUsername, venueSearchUsername, imgSearchUsername, _, _, _, webfileDcId, suggestedLangCode, langPackVersion, baseLangPackVersion, reactionsDefault, autologinToken):
var addressList: [Int: [MTDatacenterAddress]] = [:]
for option in dcOptions {
switch option {
case let .dcOption(flags, id, ipAddress, port, secret):
let preferForMedia = (flags & (1 << 1)) != 0
if addressList[Int(id)] == nil {
addressList[Int(id)] = []
}
let restrictToTcp = (flags & (1 << 2)) != 0
let isCdn = (flags & (1 << 3)) != 0
let preferForProxy = (flags & (1 << 4)) != 0
addressList[Int(id)]!.append(MTDatacenterAddress(ip: ipAddress, port: UInt16(port), preferForMedia: preferForMedia, restrictToTcp: restrictToTcp, cdn: isCdn, preferForProxy: preferForProxy, secret: secret?.makeData()))
}
}
network.context.performBatchUpdates {
for (id, list) in addressList {
network.context.updateAddressSetForDatacenter(withId: id, addressSet: MTDatacenterAddressSet(addressList: list), forceUpdateSchemes: false)
}
}
let blockedMode = (flags & (1 << 8)) != 0
updateNetworkSettingsInteractively(transaction: transaction, network: network, { settings in
var settings = settings
settings.reducedBackupDiscoveryTimeout = blockedMode
settings.applicationUpdateUrlPrefix = autoupdateUrlPrefix
return settings
})
updateRemoteStorageConfiguration(transaction: transaction, configuration: RemoteStorageConfiguration(webDocumentsHostDatacenterId: webfileDcId))
transaction.updatePreferencesEntry(key: PreferencesKeys.suggestedLocalization, { entry in
var currentLanguageCode: String?
if let entry = entry?.get(SuggestedLocalizationEntry.self) {
currentLanguageCode = entry.languageCode
}
if currentLanguageCode != suggestedLangCode {
if let suggestedLangCode = suggestedLangCode {
return PreferencesEntry(SuggestedLocalizationEntry(languageCode: suggestedLangCode, isSeen: false))
} else {
return nil
}
}
return entry
})
updateLimitsConfiguration(transaction: transaction, configuration: LimitsConfiguration(maxGroupMemberCount: chatSizeMax, maxSupergroupMemberCount: megagroupSizeMax, maxMessageForwardBatchSize: forwardedCountMax, maxRecentStickerCount: stickersRecentLimit, maxMessageEditingInterval: editTimeLimit, canRemoveIncomingMessagesInPrivateChats: (flags & (1 << 6)) != 0, maxMessageRevokeInterval: revokeTimeLimit, maxMessageRevokeIntervalInPrivateChats: revokePmTimeLimit))
updateSearchBotsConfiguration(transaction: transaction, configuration: SearchBotsConfiguration(imageBotUsername: imgSearchUsername, gifBotUsername: gifSearchUsername, venueBotUsername: venueSearchUsername))
updateLinksConfiguration(transaction: transaction, configuration: LinksConfiguration(autologinToken: autologinToken))
if let defaultReaction = reactionsDefault, let reaction = MessageReaction.Reaction(apiReaction: defaultReaction) {
updateReactionSettings(transaction: transaction, { settings in
var settings = settings
settings.quickReaction = reaction
return settings
})
}
let messageAutoremoveSeconds: Int32?
switch defaultHistoryTtl {
case let .defaultHistoryTTL(period):
if period != 0 {
messageAutoremoveSeconds = period
} else {
messageAutoremoveSeconds = nil
}
default:
messageAutoremoveSeconds = nil
}
updateGlobalMessageAutoremoveTimeoutSettings(transaction: transaction, { settings in
var settings = settings
settings.messageAutoremoveTimeout = messageAutoremoveSeconds
return settings
})
return accountManager.transaction { transaction -> Signal<Void, NoError> in
let (primary, secondary) = getLocalization(transaction)
var invalidateLocalization = false
if primary.version != langPackVersion {
invalidateLocalization = true
}
if let secondary = secondary, let baseLangPackVersion = baseLangPackVersion {
if secondary.version != baseLangPackVersion {
invalidateLocalization = true
}
}
if invalidateLocalization {
return postbox.transaction { transaction -> Void in
addSynchronizeLocalizationUpdatesOperation(transaction: transaction)
}
} else {
return .complete()
}
}
|> switchToLatest
}
}
|> switchToLatest
}).start(completed: {
subscriber.putCompletion()
})
}
return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
@@ -0,0 +1,787 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
import CryptoUtils
private struct Md5Hash: Hashable {
public let data: Data
public init(data: Data) {
precondition(data.count == 16)
self.data = data
}
}
private func md5Hash(_ data: Data) -> Md5Hash {
let hashData = data.withUnsafeBytes { bytes -> Data in
return CryptoMD5(bytes.baseAddress!, Int32(bytes.count))
}
return Md5Hash(data: hashData)
}
func md5StringHash(_ string: String) -> UInt64 {
guard let data = string.data(using: .utf8) else {
return 0
}
let hash = md5Hash(data).data
return hash.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> UInt64 in
let bytes = buffer.baseAddress!.assumingMemoryBound(to: UInt8.self)
var result: UInt64 = 0
for i in 0 ... 7 {
result += UInt64(bitPattern: Int64(bytes[i])) << (56 - 8 * i)
}
return result
}
}
private final class ManagedConsumePersonalMessagesActionsHelper {
var operationDisposables: [MessageId: Disposable] = [:]
var validateDisposables: [InvalidatedMessageHistoryTagsSummaryEntry: Disposable] = [:]
func update(entries: [PendingMessageActionsEntry], invalidateEntries: Set<InvalidatedMessageHistoryTagsSummaryEntry>) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)], beginValidateOperations: [(InvalidatedMessageHistoryTagsSummaryEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PendingMessageActionsEntry, MetaDisposable)] = []
var beginValidateOperations: [(InvalidatedMessageHistoryTagsSummaryEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validIds = Set<MessageId>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.id.peerId) {
hasRunningOperationForPeerId.insert(entry.id.peerId)
validIds.insert(entry.id)
if self.operationDisposables[entry.id] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.id] = disposable
}
}
}
var removeMergedIds: [MessageId] = []
for (id, disposable) in self.operationDisposables {
if !validIds.contains(id) {
removeMergedIds.append(id)
disposeOperations.append(disposable)
}
}
for id in removeMergedIds {
self.operationDisposables.removeValue(forKey: id)
}
var validInvalidateEntries = Set<InvalidatedMessageHistoryTagsSummaryEntry>()
for entry in invalidateEntries {
if !hasRunningOperationForPeerId.contains(entry.key.peerId) {
validInvalidateEntries.insert(entry)
if self.validateDisposables[entry] == nil {
let disposable = MetaDisposable()
beginValidateOperations.append((entry, disposable))
self.validateDisposables[entry] = disposable
}
}
}
var removeValidateEntries: [InvalidatedMessageHistoryTagsSummaryEntry] = []
for (entry, disposable) in self.validateDisposables {
if !validInvalidateEntries.contains(entry) {
removeValidateEntries.append(entry)
disposeOperations.append(disposable)
}
}
for entry in removeValidateEntries {
self.validateDisposables.removeValue(forKey: entry)
}
return (disposeOperations, beginOperations, beginValidateOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenAction<T: PendingMessageActionData>(postbox: Postbox, type: PendingMessageActionType, actionType: T.Type, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PendingMessageActionsEntry?
if let action = transaction.getPendingMessageAction(type: type, id: id) as? T {
result = PendingMessageActionsEntry(id: id, action: action)
}
return f(transaction, result)
} |> switchToLatest
}
func managedConsumePersonalMessagesActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedConsumePersonalMessagesActionsHelper>(value: ManagedConsumePersonalMessagesActionsHelper())
let actionsKey = PostboxViewKey.pendingMessageActions(type: .consumeUnseenPersonalMessage)
let invalidateKey = PostboxViewKey.invalidatedMessageHistoryTagSummaries(peerId: nil, threadId: nil, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud)
let disposable = postbox.combinedView(keys: [actionsKey, invalidateKey]).start(next: { view in
var entries: [PendingMessageActionsEntry] = []
var invalidateEntries = Set<InvalidatedMessageHistoryTagsSummaryEntry>()
if let v = view.views[actionsKey] as? PendingMessageActionsView {
entries = v.entries
}
if let v = view.views[invalidateKey] as? InvalidatedMessageHistoryTagSummariesView {
invalidateEntries = v.entries
}
let (disposeOperations, beginOperations, beginValidateOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)], beginValidateOperations: [(InvalidatedMessageHistoryTagsSummaryEntry, MetaDisposable)]) in
return helper.update(entries: entries, invalidateEntries: invalidateEntries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenAction(postbox: postbox, type: .consumeUnseenPersonalMessage, actionType: ConsumePersonalMessageAction.self, id: entry.id, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let _ = entry.action as? ConsumePersonalMessageAction {
return synchronizeConsumeMessageContents(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, id: entry.id)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .consumeUnseenPersonalMessage, id: entry.id, action: nil)
})
disposable.set(signal.start())
}
for (entry, disposable) in beginValidateOperations {
let signal = synchronizeUnseenPersonalMentionsTag(postbox: postbox, network: network, entry: entry)
|> then(postbox.transaction { transaction -> Void in
transaction.removeInvalidatedMessageHistoryTagsSummaryEntry(entry)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
func managedReadReactionActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedConsumePersonalMessagesActionsHelper>(value: ManagedConsumePersonalMessagesActionsHelper())
let actionsKey = PostboxViewKey.pendingMessageActions(type: .readReaction)
let invalidateKey = PostboxViewKey.invalidatedMessageHistoryTagSummaries(peerId: nil, threadId: nil, tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud)
let disposable = postbox.combinedView(keys: [actionsKey, invalidateKey]).start(next: { view in
var entries: [PendingMessageActionsEntry] = []
var invalidateEntries = Set<InvalidatedMessageHistoryTagsSummaryEntry>()
if let v = view.views[actionsKey] as? PendingMessageActionsView {
entries = v.entries
}
if let v = view.views[invalidateKey] as? InvalidatedMessageHistoryTagSummariesView {
invalidateEntries = v.entries
}
let (disposeOperations, beginOperations, beginValidateOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)], beginValidateOperations: [(InvalidatedMessageHistoryTagsSummaryEntry, MetaDisposable)]) in
return helper.update(entries: entries, invalidateEntries: invalidateEntries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenAction(postbox: postbox, type: .readReaction, actionType: ReadReactionAction.self, id: entry.id, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let _ = entry.action as? ReadReactionAction {
return synchronizeReadMessageReactions(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, id: entry.id)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .readReaction, id: entry.id, action: nil)
})
disposable.set(signal.start())
}
for (entry, disposable) in beginValidateOperations {
let signal = synchronizeUnseenReactionsTag(postbox: postbox, network: network, entry: entry)
|> then(postbox.transaction { transaction -> Void in
transaction.removeInvalidatedMessageHistoryTagsSummaryEntry(entry)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func synchronizeConsumeMessageContents(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal<Void, NoError> {
if id.peerId.namespace == Namespaces.Peer.CloudUser || id.peerId.namespace == Namespaces.Peer.CloudGroup {
return network.request(Api.functions.messages.readMessageContents(id: [id.id]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AffectedMessages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .affectedMessages(pts, ptsCount):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
}
}
return postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .consumeUnseenPersonalMessage, id: id, action: nil)
transaction.updateMessage(id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ConsumablePersonalMentionMessageAttribute, !attribute.consumed {
attributes[j] = ConsumablePersonalMentionMessageAttribute(consumed: true, pending: false)
break loop
}
}
var updatedTags = currentMessage.tags
updatedTags.remove(.unseenPersonalMessage)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
} else if id.peerId.namespace == Namespaces.Peer.CloudChannel {
if let peer = transaction.getPeer(id.peerId), let inputChannel = apiInputChannel(peer) {
return network.request(Api.functions.channels.readMessageContents(channel: inputChannel, id: [id.id]))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
} |> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .consumeUnseenPersonalMessage, id: id, action: nil)
transaction.updateMessage(id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ConsumablePersonalMentionMessageAttribute, !attribute.consumed {
attributes[j] = ConsumablePersonalMentionMessageAttribute(consumed: true, pending: false)
break loop
}
}
var updatedTags = currentMessage.tags
updatedTags.remove(.unseenPersonalMessage)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
} else {
return .complete()
}
} else {
return .complete()
}
}
private func synchronizeReadMessageReactions(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal<Void, NoError> {
if id.peerId.namespace == Namespaces.Peer.CloudUser || id.peerId.namespace == Namespaces.Peer.CloudGroup {
return network.request(Api.functions.messages.readMessageContents(id: [id.id]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AffectedMessages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .affectedMessages(pts, ptsCount):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
}
}
return postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .readReaction, id: id, action: nil)
transaction.updateMessage(id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute, attribute.hasUnseen {
attributes[j] = attribute.withAllSeen()
break loop
}
}
var updatedTags = currentMessage.tags
updatedTags.remove(.unseenReaction)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
} else if id.peerId.namespace == Namespaces.Peer.CloudChannel {
if let peer = transaction.getPeer(id.peerId), let inputChannel = apiInputChannel(peer) {
return network.request(Api.functions.channels.readMessageContents(channel: inputChannel, id: [id.id]))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .readReaction, id: id, action: nil)
transaction.updateMessage(id, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute, attribute.hasUnseen {
attributes[j] = attribute.withAllSeen()
break loop
}
}
var updatedTags = currentMessage.tags
updatedTags.remove(.unseenReaction)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: updatedTags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
}
} else {
return .complete()
}
} else {
return .complete()
}
}
private func synchronizeUnseenPersonalMentionsTag(postbox: Postbox, network: Network, entry: InvalidatedMessageHistoryTagsSummaryEntry) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
if let peer = transaction.getPeer(entry.key.peerId), let inputPeer = apiInputPeer(peer) {
return network.request(Api.functions.messages.getPeerDialogs(peers: [.inputDialogPeer(peer: inputPeer)]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.PeerDialogs?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .peerDialogs(dialogs, _, _, _, _):
if let dialog = dialogs.filter({ $0.peerId == entry.key.peerId }).first {
let apiTopMessage: Int32
let apiUnreadMentionsCount: Int32
switch dialog {
case let .dialog(_, _, topMessage, _, _, _, unreadMentionsCount, _, _, _, _, _, _):
apiTopMessage = topMessage
apiUnreadMentionsCount = unreadMentionsCount
case .dialogFolder:
assertionFailure()
return .complete()
}
return postbox.transaction { transaction -> Void in
transaction.replaceMessageTagSummary(peerId: entry.key.peerId, threadId: nil, tagMask: entry.key.tagMask, namespace: entry.key.namespace, customTag: nil, count: apiUnreadMentionsCount, maxId: apiTopMessage)
}
} else {
return .complete()
}
}
} else {
return .complete()
}
}
} else {
return .complete()
}
} |> switchToLatest
}
private func synchronizeUnseenReactionsTag(postbox: Postbox, network: Network, entry: InvalidatedMessageHistoryTagsSummaryEntry) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
if let peer = transaction.getPeer(entry.key.peerId), let inputPeer = apiInputPeer(peer) {
return network.request(Api.functions.messages.getPeerDialogs(peers: [.inputDialogPeer(peer: inputPeer)]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.PeerDialogs?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .peerDialogs(dialogs, _, _, _, _):
if let dialog = dialogs.filter({ $0.peerId == entry.key.peerId }).first {
let apiTopMessage: Int32
let apiUnreadReactionsCount: Int32
switch dialog {
case let .dialog(_, _, topMessage, _, _, _, _, unreadReactionsCount, _, _, _, _, _):
apiTopMessage = topMessage
apiUnreadReactionsCount = unreadReactionsCount
case .dialogFolder:
assertionFailure()
return .complete()
}
return postbox.transaction { transaction -> Void in
transaction.replaceMessageTagSummary(peerId: entry.key.peerId, threadId: nil, tagMask: entry.key.tagMask, namespace: entry.key.namespace, customTag: nil, count: apiUnreadReactionsCount, maxId: apiTopMessage)
}
} else {
return .complete()
}
}
} else {
return .complete()
}
}
} else {
return .complete()
}
} |> switchToLatest
}
func managedSynchronizeMessageHistoryTagSummaries(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, threadId: Int64?) -> Signal<Void, NoError> {
let accountPeerId = stateManager.accountPeerId
return Signal { _ in
let helper = Atomic<ManagedConsumePersonalMessagesActionsHelper>(value: ManagedConsumePersonalMessagesActionsHelper())
let invalidateKey = PostboxViewKey.invalidatedMessageHistoryTagSummaries(peerId: peerId, threadId: threadId, tagMask: MessageTags(rawValue: 0), namespace: Namespaces.Message.Cloud)
let disposable = postbox.combinedView(keys: [invalidateKey]).start(next: { view in
var invalidateEntries = Set<InvalidatedMessageHistoryTagsSummaryEntry>()
if let v = view.views[invalidateKey] as? InvalidatedMessageHistoryTagSummariesView {
invalidateEntries = v.entries
}
if invalidateEntries.contains(where: { $0.key.customTag != nil }) {
invalidateEntries = invalidateEntries.filter({ $0.key.customTag == nil })
invalidateEntries.insert(InvalidatedMessageHistoryTagsSummaryEntry(key: InvalidatedMessageHistoryTagsSummaryKey(peerId: peerId, namespace: Namespaces.Message.Cloud, tagMask: [], threadId: threadId, customTag: MemoryBuffer()), version: 0))
}
let (disposeOperations, _, beginValidateOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)], beginValidateOperations: [(InvalidatedMessageHistoryTagsSummaryEntry, MetaDisposable)]) in
return helper.update(entries: [], invalidateEntries: invalidateEntries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginValidateOperations {
if entry.key.customTag != nil {
if peerId == stateManager.accountPeerId {
let signal = synchronizeSavedMessageTags(postbox: postbox, network: network, peerId: peerId, threadId: entry.key.threadId, force: false)
|> map { _ -> Void in
}
|> then(postbox.transaction { transaction -> Void in
transaction.removeInvalidatedMessageHistoryTagsSummaryEntriesWithCustomTags(peerId: peerId, threadId: entry.key.threadId, namespace: Namespaces.Message.Cloud, tagMask: [])
})
disposable.set(signal.start())
} else {
assertionFailure()
let signal = postbox.transaction { transaction -> Void in
transaction.removeInvalidatedMessageHistoryTagsSummaryEntriesWithCustomTags(peerId: peerId, threadId: entry.key.threadId, namespace: Namespaces.Message.Cloud, tagMask: [])
}
disposable.set(signal.start())
}
} else {
let signal = synchronizeMessageHistoryTagSummary(accountPeerId: accountPeerId, postbox: postbox, network: network, entry: entry)
|> then(postbox.transaction { transaction -> Void in
transaction.removeInvalidatedMessageHistoryTagsSummaryEntry(entry)
})
disposable.set(signal.start())
}
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func synchronizeMessageHistoryTagSummary(accountPeerId: PeerId, postbox: Postbox, network: Network, entry: InvalidatedMessageHistoryTagsSummaryEntry) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
if let threadId = entry.key.threadId {
if let peer = transaction.getPeer(entry.key.peerId), peer.isForum, !peer.isMonoForum, let inputPeer = apiInputPeer(peer) {
return network.request(Api.functions.messages.getReplies(peer: inputPeer, msgId: Int32(clamping: threadId), offsetId: 0, offsetDate: 0, addOffset: 0, limit: 1, maxId: 0, minId: 0, hash: 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.Messages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
guard let result = result else {
return .complete()
}
return postbox.transaction { transaction -> Void in
switch result {
case let .channelMessages(_, _, count, _, messages, _, _, _):
let topId: Int32 = messages.first?.id(namespace: Namespaces.Message.Cloud)?.id ?? 1
transaction.replaceMessageTagSummary(peerId: entry.key.peerId, threadId: threadId, tagMask: entry.key.tagMask, namespace: entry.key.namespace, customTag: nil, count: count, maxId: topId)
default:
break
}
}
}
} else {
return .complete()
}
} else {
if entry.key.peerId != accountPeerId {
return .single(Void())
}
if let peer = transaction.getPeer(entry.key.peerId), let inputPeer = apiInputPeer(peer) {
return network.request(Api.functions.messages.search(flags: 0, peer: inputPeer, q: "", fromId: nil, savedPeerId: nil, savedReaction: nil, topMsgId: nil, filter: .inputMessagesFilterEmpty, minDate: 0, maxDate: 0, offsetId: 0, addOffset: 0, limit: 1, maxId: 0, minId: 0, hash: 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.Messages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
if let result {
let apiMessages: [Api.Message]
let apiCount: Int32
switch result {
case let .channelMessages(_, _, count, _, messages, _, _, _):
apiMessages = messages
apiCount = count
case let .messages(messages, _, _, _):
apiMessages = messages
apiCount = Int32(messages.count)
case let .messagesNotModified(count):
apiMessages = []
apiCount = count
case let .messagesSlice(_, count, _, _, _, messages, _, _, _):
apiMessages = messages
apiCount = count
}
let topMessageId = apiMessages.first?.id(namespace: Namespaces.Message.Cloud)?.id ?? 1
transaction.replaceMessageTagSummary(peerId: entry.key.peerId, threadId: nil, tagMask: entry.key.tagMask, namespace: entry.key.namespace, customTag: nil, count: apiCount, maxId: topMessageId)
}
}
}
} else {
return .complete()
}
}
}
|> switchToLatest
}
func synchronizeSavedMessageTags(postbox: Postbox, network: Network, peerId: PeerId, threadId: Int64?, force: Bool) -> Signal<Never, NoError> {
let key: PostboxViewKey = .pendingMessageActions(type: .updateReaction)
let waitForApplySignal: Signal<Never, NoError> = postbox.combinedView(keys: [key])
|> map { views -> Bool in
guard let view = views.views[key] as? PendingMessageActionsView else {
return false
}
for entry in view.entries {
if entry.id.peerId == peerId {
return false
}
}
return true
}
|> filter { $0 }
|> take(1)
|> ignoreValues
let updateSignal: Signal<Never, NoError> = (postbox.transaction { transaction -> (Bool, Peer?, Int64) in
struct HashableTag {
var titleId: UInt64?
var count: Int
var id: UInt64
init(titleId: UInt64?, count: Int, id: UInt64) {
self.titleId = titleId
self.count = count
self.id = id
}
}
let savedTags = _internal_savedMessageTags(transaction: transaction)
var hashableTags: [HashableTag] = []
for tag in transaction.getMessageTagSummaryCustomTags(peerId: peerId, threadId: threadId, tagMask: [], namespace: Namespaces.Message.Cloud) {
if let summary = transaction.getMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: [], namespace: Namespaces.Message.Cloud, customTag: tag), summary.count > 0 {
guard let reaction = ReactionsMessageAttribute.reactionFromMessageTag(tag: tag) else {
continue
}
var tagTitle: String?
if threadId == nil, let savedTags {
if let value = savedTags.tags.first(where: { $0.reaction == reaction }) {
tagTitle = value.title
}
}
let reactionId: UInt64
switch reaction {
case let .custom(id):
reactionId = UInt64(bitPattern: id)
case let .builtin(string):
reactionId = md5StringHash(string)
case .stars:
reactionId = md5StringHash("star")
}
var titleId: UInt64?
if let tagTitle {
titleId = md5StringHash(tagTitle)
}
hashableTags.append(HashableTag(
titleId: titleId,
count: Int(summary.count),
id: reactionId
))
}
}
hashableTags.sort(by: { lhs, rhs in
if lhs.count != rhs.count {
return lhs.count > rhs.count
}
return lhs.id < rhs.id
})
var hashIds: [UInt64] = []
for tag in hashableTags {
hashIds.append(tag.id)
if let titleId = tag.titleId {
hashIds.append(titleId)
}
hashIds.append(UInt64(tag.count))
}
var hashAcc: UInt64 = 0
for id in hashIds {
combineInt64Hash(&hashAcc, with: id)
}
return (
transaction.getPreferencesEntry(key: PreferencesKeys.didCacheSavedMessageTags(threadId: threadId)) != nil,
threadId.flatMap { transaction.getPeer(PeerId($0)) },
Int64(bitPattern: hashAcc)
)
}
|> mapToSignal { alreadyCached, subPeer, currentHash -> Signal<Never, NoError> in
if alreadyCached && !force {
return .complete()
}
let inputSubPeer = subPeer.flatMap(apiInputPeer)
if threadId != nil && inputSubPeer == nil {
return .complete()
}
var flags: Int32 = 0
if inputSubPeer != nil {
flags |= 1 << 0
}
return network.request(Api.functions.messages.getSavedReactionTags(flags: flags, peer: inputSubPeer, hash: currentHash))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.SavedReactionTags?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
guard let result = result else {
return .complete()
}
switch result {
case .savedReactionTagsNotModified:
return postbox.transaction { transaction -> Void in
transaction.setPreferencesEntry(key: PreferencesKeys.didCacheSavedMessageTags(threadId: threadId), value: PreferencesEntry(data: Data()))
}
|> ignoreValues
case let .savedReactionTags(tags, _):
var customFileIds: [Int64] = []
var parsedTags: [SavedMessageTags.Tag] = []
for tag in tags {
switch tag {
case let .savedReactionTag(_, reaction, title, count):
guard let reaction = MessageReaction.Reaction(apiReaction: reaction) else {
continue
}
parsedTags.append(SavedMessageTags.Tag(
reaction: reaction,
title: title,
count: Int(count)
))
if case let .custom(fileId) = reaction {
customFileIds.append(fileId)
}
}
}
return postbox.transaction { transaction -> Void in
if threadId == nil {
_internal_setSavedMessageTags(transaction: transaction, savedMessageTags: SavedMessageTags(
hash: 0,
tags: parsedTags
))
}
let previousTags = transaction.getMessageTagSummaryCustomTags(peerId: peerId, threadId: threadId, tagMask: [], namespace: Namespaces.Message.Cloud)
let topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.Cloud)?.id ?? 1
var validTags: [MemoryBuffer] = []
for tag in parsedTags {
let customTag = ReactionsMessageAttribute.messageTag(reaction: tag.reaction)
validTags.append(customTag)
transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: [], namespace: Namespaces.Message.Cloud, customTag: customTag, count: Int32(tag.count), maxId: topMessageId)
}
for tag in previousTags {
if !validTags.contains(tag) {
transaction.replaceMessageTagSummary(peerId: peerId, threadId: threadId, tagMask: [], namespace: Namespaces.Message.Cloud, customTag: tag, count: 0, maxId: topMessageId)
}
}
transaction.setPreferencesEntry(key: PreferencesKeys.didCacheSavedMessageTags(threadId: threadId), value: PreferencesEntry(data: Data()))
}
|> ignoreValues
}
}
})
return waitForApplySignal |> then(updateSignal |> delay(1.0, queue: .concurrentDefaultQueue()))
}
@@ -0,0 +1,431 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public func updateGlobalNotificationSettingsInteractively(postbox: Postbox, _ f: @escaping (GlobalNotificationSettingsSet) -> GlobalNotificationSettingsSet) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.globalNotifications, { current in
if let current = current?.get(GlobalNotificationSettings.self) {
return PreferencesEntry(GlobalNotificationSettings(toBeSynchronized: f(current.effective), remote: current.remote))
} else {
let settings = f(GlobalNotificationSettingsSet.defaultSettings)
return PreferencesEntry(GlobalNotificationSettings(toBeSynchronized: settings, remote: settings))
}
})
transaction.globalNotificationSettingsUpdated()
}
}
public func resetPeerNotificationSettings(network: Network) -> Signal<Void, NoError> {
return network.request(Api.functions.account.resetNotifySettings())
|> retryRequestIfNotFrozen
|> mapToSignal { _ in return Signal<Void, NoError>.complete() }
}
private enum SynchronizeGlobalSettingsData: Equatable {
case none
case fetch
case push(GlobalNotificationSettingsSet)
static func ==(lhs: SynchronizeGlobalSettingsData, rhs: SynchronizeGlobalSettingsData) -> Bool {
switch lhs {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case .fetch:
if case .fetch = rhs {
return true
} else {
return false
}
case let .push(settings):
if case .push(settings) = rhs {
return true
} else {
return false
}
}
}
}
func managedGlobalNotificationSettings(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let data = postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
|> map { view -> SynchronizeGlobalSettingsData in
if let preferences = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
if let settings = preferences.toBeSynchronized {
return .push(settings)
} else {
return .none
}
} else {
return .fetch
}
}
let action = data
|> distinctUntilChanged
|> mapToSignal { data -> Signal<Void, NoError> in
switch data {
case .none:
return .complete()
case .fetch:
return fetchedNotificationSettings(network: network)
|> mapToSignal { settings -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.globalNotifications, { current in
if let current = current?.get(GlobalNotificationSettings.self) {
return PreferencesEntry(GlobalNotificationSettings(toBeSynchronized: current.toBeSynchronized, remote: settings))
} else {
return PreferencesEntry(GlobalNotificationSettings(toBeSynchronized: nil, remote: settings))
}
})
transaction.globalNotificationSettingsUpdated()
}
}
case let .push(settings):
return pushedNotificationSettings(network: network, settings: settings)
|> then(postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.globalNotifications, { current in
if let current = current?.get(GlobalNotificationSettings.self), current.toBeSynchronized == settings {
return PreferencesEntry(GlobalNotificationSettings(toBeSynchronized: nil, remote: settings))
} else {
return current
}
})
transaction.globalNotificationSettingsUpdated()
})
}
}
return action
}
private func fetchedNotificationSettings(network: Network) -> Signal<GlobalNotificationSettingsSet, NoError> {
let chats = network.request(Api.functions.account.getNotifySettings(peer: Api.InputNotifyPeer.inputNotifyChats))
let users = network.request(Api.functions.account.getNotifySettings(peer: Api.InputNotifyPeer.inputNotifyUsers))
let channels = network.request(Api.functions.account.getNotifySettings(peer: Api.InputNotifyPeer.inputNotifyBroadcasts))
let contactsJoinedMuted = network.request(Api.functions.account.getContactSignUpNotification())
let reactions = network.request(Api.functions.account.getReactionsNotifySettings())
return combineLatest(chats, users, channels, contactsJoinedMuted, reactions)
|> retryRequestIfNotFrozen
|> mapToSignal { data in
guard let (chats, users, channels, contactsJoinedMuted, reactions) = data else {
return .complete()
}
let chatsSettings: MessageNotificationSettings
switch chats {
case let .peerNotifySettings(_, showPreviews, _, muteUntil, iosSound, _, desktopSound, storiesMuted, storiesHideSender, storiesIosSound, _, storiesDesktopSound):
let sound: Api.NotificationSound?
let storiesSound: Api.NotificationSound?
#if os(iOS)
sound = iosSound
storiesSound = storiesIosSound
#elseif os(macOS)
sound = desktopSound
storiesSound = storiesDesktopSound
#endif
let enabled: Bool
if muteUntil != nil && muteUntil != 0 {
enabled = false
} else {
enabled = true
}
let displayPreviews: Bool
if let showPreviews = showPreviews, case .boolFalse = showPreviews {
displayPreviews = false
} else {
displayPreviews = true
}
let storiesMutedValue: PeerStoryNotificationSettings.Mute
if let storiesMuted = storiesMuted {
storiesMutedValue = storiesMuted == .boolTrue ? .muted : .unmuted
} else {
storiesMutedValue = .default
}
var storiesHideSenderValue: PeerStoryNotificationSettings.HideSender
if let storiesHideSender = storiesHideSender {
storiesHideSenderValue = storiesHideSender == .boolTrue ? .hide : .show
} else {
storiesHideSenderValue = .default
}
chatsSettings = MessageNotificationSettings(
enabled: enabled,
displayPreviews: displayPreviews,
sound: PeerMessageSound(apiSound: sound ?? .notificationSoundDefault),
storySettings: PeerStoryNotificationSettings(
mute: storiesMutedValue,
hideSender: storiesHideSenderValue,
sound: PeerMessageSound(apiSound: sound ?? .notificationSoundDefault)
)
)
}
let userSettings: MessageNotificationSettings
switch users {
case let .peerNotifySettings(_, showPreviews, _, muteUntil, iosSound, _, desktopSound, storiesMuted, storiesHideSender, storiesIosSound, _, storiesDesktopSound):
let sound: Api.NotificationSound?
let storiesSound: Api.NotificationSound?
#if os(iOS)
sound = iosSound
storiesSound = storiesIosSound
#elseif os(macOS)
sound = desktopSound
storiesSound = storiesDesktopSound
#endif
let enabled: Bool
if muteUntil != nil && muteUntil != 0 {
enabled = false
} else {
enabled = true
}
let displayPreviews: Bool
if let showPreviews = showPreviews, case .boolFalse = showPreviews {
displayPreviews = false
} else {
displayPreviews = true
}
let storiesMutedValue: PeerStoryNotificationSettings.Mute
if let storiesMuted = storiesMuted {
storiesMutedValue = storiesMuted == .boolTrue ? .muted : .unmuted
} else {
storiesMutedValue = .default
}
var storiesHideSenderValue: PeerStoryNotificationSettings.HideSender
if let storiesHideSender = storiesHideSender {
storiesHideSenderValue = storiesHideSender == .boolTrue ? .hide : .show
} else {
storiesHideSenderValue = .default
}
userSettings = MessageNotificationSettings(
enabled: enabled,
displayPreviews: displayPreviews,
sound: PeerMessageSound(apiSound: sound ?? .notificationSoundDefault),
storySettings: PeerStoryNotificationSettings(
mute: storiesMutedValue,
hideSender: storiesHideSenderValue,
sound: PeerMessageSound(apiSound: sound ?? .notificationSoundDefault)
)
)
}
let channelSettings: MessageNotificationSettings
switch channels {
case let .peerNotifySettings(_, showPreviews, _, muteUntil, iosSound, _, desktopSound, storiesMuted, storiesHideSender, storiesIosSound, _, storiesDesktopSound):
let sound: Api.NotificationSound?
let storiesSound: Api.NotificationSound?
#if os(iOS)
sound = iosSound
storiesSound = storiesIosSound
#elseif os(macOS)
sound = desktopSound
storiesSound = storiesDesktopSound
#endif
let enabled: Bool
if muteUntil != nil && muteUntil != 0 {
enabled = false
} else {
enabled = true
}
let displayPreviews: Bool
if let showPreviews = showPreviews, case .boolFalse = showPreviews {
displayPreviews = false
} else {
displayPreviews = true
}
let storiesMutedValue: PeerStoryNotificationSettings.Mute
if let storiesMuted = storiesMuted {
storiesMutedValue = storiesMuted == .boolTrue ? .muted : .unmuted
} else {
storiesMutedValue = .default
}
var storiesHideSenderValue: PeerStoryNotificationSettings.HideSender
if let storiesHideSender = storiesHideSender {
storiesHideSenderValue = storiesHideSender == .boolTrue ? .hide : .show
} else {
storiesHideSenderValue = .default
}
channelSettings = MessageNotificationSettings(
enabled: enabled,
displayPreviews: displayPreviews,
sound: PeerMessageSound(apiSound: sound ?? .notificationSoundDefault),
storySettings: PeerStoryNotificationSettings(
mute: storiesMutedValue,
hideSender: storiesHideSenderValue,
sound: PeerMessageSound(apiSound: sound ?? .notificationSoundDefault)
)
)
}
let reactionSettings: PeerReactionNotificationSettings
switch reactions {
case let .reactionsNotifySettings(_, messagesNotifyFrom, storiesNotifyFrom, sound, showPreviews):
let mappedMessages: PeerReactionNotificationSettings.Sources
if let messagesNotifyFrom {
switch messagesNotifyFrom {
case .reactionNotificationsFromAll:
mappedMessages = .everyone
case .reactionNotificationsFromContacts:
mappedMessages = .contacts
}
} else {
mappedMessages = .nobody
}
let mappedStories: PeerReactionNotificationSettings.Sources
if let storiesNotifyFrom {
switch storiesNotifyFrom {
case .reactionNotificationsFromAll:
mappedStories = .everyone
case .reactionNotificationsFromContacts:
mappedStories = .contacts
}
} else {
mappedStories = .nobody
}
reactionSettings = PeerReactionNotificationSettings(
messages: mappedMessages,
stories: mappedStories,
hideSender: showPreviews == .boolFalse ? .hide : .show,
sound: PeerMessageSound(apiSound: sound)
)
}
return .single(GlobalNotificationSettingsSet(privateChats: userSettings, groupChats: chatsSettings, channels: channelSettings, reactionSettings: reactionSettings, contactsJoined: contactsJoinedMuted == .boolFalse))
}
}
private func apiInputPeerNotifySettings(_ settings: MessageNotificationSettings) -> Api.InputPeerNotifySettings {
let muteUntil: Int32?
if settings.enabled {
muteUntil = 0
} else {
muteUntil = Int32.max
}
let sound: Api.NotificationSound? = settings.sound.apiSound
var flags: Int32 = 0
flags |= (1 << 0)
if muteUntil != nil {
flags |= (1 << 2)
}
if sound != nil {
flags |= (1 << 3)
}
let storiesMuted: Api.Bool?
switch settings.storySettings.mute {
case .default:
storiesMuted = nil
case .muted:
storiesMuted = .boolTrue
case .unmuted:
storiesMuted = .boolFalse
}
if storiesMuted != nil {
flags |= (1 << 6)
}
let storiesHideSender: Api.Bool?
switch settings.storySettings.hideSender {
case .default:
storiesHideSender = nil
case .hide:
storiesHideSender = .boolTrue
case .show:
storiesHideSender = .boolFalse
}
if storiesHideSender != nil {
flags |= (1 << 7)
}
let storiesSound: Api.NotificationSound? = settings.storySettings.sound.apiSound
if storiesSound != nil {
flags |= (1 << 8)
}
return .inputPeerNotifySettings(flags: flags, showPreviews: settings.displayPreviews ? .boolTrue : .boolFalse, silent: nil, muteUntil: muteUntil, sound: sound, storiesMuted: storiesMuted, storiesHideSender: storiesHideSender, storiesSound: storiesSound)
}
private func pushedNotificationSettings(network: Network, settings: GlobalNotificationSettingsSet) -> Signal<Void, NoError> {
let pushedChats = network.request(Api.functions.account.updateNotifySettings(peer: Api.InputNotifyPeer.inputNotifyChats, settings: apiInputPeerNotifySettings(settings.groupChats)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
let pushedUsers = network.request(Api.functions.account.updateNotifySettings(peer: Api.InputNotifyPeer.inputNotifyUsers, settings: apiInputPeerNotifySettings(settings.privateChats)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
let pushedChannels = network.request(Api.functions.account.updateNotifySettings(peer: Api.InputNotifyPeer.inputNotifyBroadcasts, settings: apiInputPeerNotifySettings(settings.channels)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
let pushedContactsJoined = network.request(Api.functions.account.setContactSignUpNotification(silent: settings.contactsJoined ? .boolFalse : .boolTrue))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
var reactionFlags: Int32 = 0
var reactionsMessages: Api.ReactionNotificationsFrom?
switch settings.reactionSettings.messages {
case .nobody:
break
case .everyone:
reactionsMessages = .reactionNotificationsFromAll
case .contacts:
reactionsMessages = .reactionNotificationsFromContacts
}
if reactionsMessages != nil {
reactionFlags |= 1 << 0
}
var reactionsStories: Api.ReactionNotificationsFrom?
switch settings.reactionSettings.stories {
case .nobody:
break
case .everyone:
reactionsStories = .reactionNotificationsFromAll
case .contacts:
reactionsStories = .reactionNotificationsFromContacts
}
if reactionsStories != nil {
reactionFlags |= 1 << 1
}
let inputReactionSettings: Api.ReactionsNotifySettings = .reactionsNotifySettings(
flags: reactionFlags,
messagesNotifyFrom: reactionsMessages,
storiesNotifyFrom: reactionsStories,
sound: settings.reactionSettings.sound.apiSound,
showPreviews: settings.reactionSettings.hideSender == .hide ? .boolFalse : .boolTrue
)
let pushedReactions = network.request(Api.functions.account.setReactionsNotifySettings(settings: inputReactionSettings))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.ReactionsNotifySettings?, NoError> in
return .single(nil)
}
return combineLatest(pushedChats, pushedUsers, pushedChannels, pushedContactsJoined, pushedReactions)
|> mapToSignal { _ -> Signal<Void, NoError> in return .complete() }
}
@@ -0,0 +1,207 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public struct PeerActivitySpace: Hashable {
public enum Category: Equatable, Hashable {
case global
case thread(Int64)
case voiceChat
}
public var peerId: PeerId
public var category: Category
public init(peerId: PeerId, category: Category) {
self.peerId = peerId
self.category = category
}
}
struct PeerInputActivityRecord: Equatable {
let activity: PeerInputActivity
let updateId: Int32
}
private final class ManagedLocalTypingActivitiesContext {
private var disposables: [PeerActivitySpace: (PeerInputActivityRecord, MetaDisposable)] = [:]
func update(activities: [PeerActivitySpace: [(PeerId, PeerInputActivityRecord)]]) -> (start: [(PeerActivitySpace, PeerInputActivityRecord?, MetaDisposable)], dispose: [MetaDisposable]) {
var start: [(PeerActivitySpace, PeerInputActivityRecord?, MetaDisposable)] = []
var dispose: [MetaDisposable] = []
var validPeerIds = Set<PeerActivitySpace>()
for (peerId, record) in activities {
if let activity = record.first?.1 {
validPeerIds.insert(peerId)
let currentRecord = self.disposables[peerId]
if currentRecord == nil || currentRecord!.0 != activity {
if let disposable = currentRecord?.1 {
dispose.append(disposable)
}
let disposable = MetaDisposable()
start.append((peerId, activity, disposable))
self.disposables[peerId] = (activity, disposable)
}
}
}
var removePeerIds: [PeerActivitySpace] = []
for key in self.disposables.keys {
if !validPeerIds.contains(key) {
removePeerIds.append(key)
}
}
for peerId in removePeerIds {
dispose.append(self.disposables[peerId]!.1)
self.disposables.removeValue(forKey: peerId)
}
return (start, dispose)
}
func dispose() {
for (_, record) in self.disposables {
record.1.dispose()
}
self.disposables.removeAll()
}
}
func managedLocalTypingActivities(activities: Signal<[PeerActivitySpace: [(PeerId, PeerInputActivityRecord)]], NoError>, postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal<Void, NoError> {
return Signal { subscriber in
let context = Atomic(value: ManagedLocalTypingActivitiesContext())
let disposable = activities.start(next: { activities in
let (start, dispose) = context.with { context in
return context.update(activities: activities)
}
for disposable in dispose {
disposable.dispose()
}
for (peerId, activity, disposable) in start {
var threadId: Int64?
switch peerId.category {
case let .thread(id):
threadId = id
default:
break
}
disposable.set(requestActivity(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId.peerId, threadId: threadId, activity: activity?.activity).start())
}
})
return ActionDisposable {
disposable.dispose()
context.with { context -> Void in
context.dispose()
}
}
}
}
private func actionFromActivity(_ activity: PeerInputActivity?) -> Api.SendMessageAction {
if let activity = activity {
switch activity {
case .typingText:
return .sendMessageTypingAction
case .recordingVoice:
return .sendMessageRecordAudioAction
case .playingGame:
return .sendMessageGamePlayAction
case let .uploadingFile(progress):
return .sendMessageUploadDocumentAction(progress: progress)
case let .uploadingPhoto(progress):
return .sendMessageUploadPhotoAction(progress: progress)
case let .uploadingVideo(progress):
return .sendMessageUploadVideoAction(progress: progress)
case .recordingInstantVideo:
return .sendMessageRecordRoundAction
case let .uploadingInstantVideo(progress):
return .sendMessageUploadRoundAction(progress: progress)
case .speakingInGroupCall:
return .speakingInGroupCallAction
case .choosingSticker:
return .sendMessageChooseStickerAction
case let .interactingWithEmoji(emoticon, messageId, interaction):
return .sendMessageEmojiInteraction(emoticon: emoticon, msgId: messageId.id, interaction: interaction?.apiDataJson ?? .dataJSON(data: ""))
case let .seeingEmojiInteraction(emoticon):
return .sendMessageEmojiInteractionSeen(emoticon: emoticon)
}
} else {
return .sendMessageCancelAction
}
}
private func requestActivity(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, threadId: Int64?, activity: PeerInputActivity?) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
if let peer = transaction.getPeer(peerId) {
if peerId == accountPeerId {
return .complete()
}
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
if let activity = activity {
switch activity {
case .speakingInGroupCall:
break
default:
return .complete()
}
}
}
if let _ = peer as? TelegramUser {
if let presence = transaction.getPeerPresence(peerId: peerId) as? TelegramUserPresence {
switch presence.status {
case .none, .lastWeek, .lastMonth:
return .complete()
case .recently:
break
case let .present(statusTimestamp):
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
if statusTimestamp < timestamp - 30 {
return .complete()
}
}
} else {
return .complete()
}
}
if let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0
let topMessageId = threadId.flatMap { Int32(clamping: $0) }
if topMessageId != nil {
flags |= 1 << 0
}
return network.request(Api.functions.messages.setTyping(flags: flags, peer: inputPeer, topMsgId: topMessageId, action: actionFromActivity(activity)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
} else if let peer = peer as? TelegramSecretChat, activity == .typingText {
let _ = PeerId(peer.id.toInt64())
return network.request(Api.functions.messages.setEncryptedTyping(peer: .inputEncryptedChat(chatId: Int32(peer.id.id._internalGetInt64Value()), accessHash: peer.accessHash), typing: .boolTrue))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
return .complete()
}
} else {
return .complete()
}
} |> switchToLatest
}
@@ -0,0 +1,302 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedLocalizationUpdatesOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeLocalizationUpdatesOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedLocalizationUpdatesOperations(accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeLocalizationUpdates
let helper = Atomic<ManagedLocalizationUpdatesOperationsHelper>(value: ManagedLocalizationUpdatesOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let _ = entry.contents as? SynchronizeLocalizationUpdatesOperation {
return synchronizeLocalizationUpdates(accountManager: accountManager, postbox: postbox, network: network)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private enum SynchronizeLocalizationUpdatesError {
case done
case reset
}
func getLocalization(_ transaction: AccountManagerModifier<TelegramAccountManagerTypes>) -> (primary: (code: String, version: Int32, entries: [LocalizationEntry]), secondary: (code: String, version: Int32, entries: [LocalizationEntry])?) {
let localizationSettings: LocalizationSettings?
if let current = transaction.getSharedData(SharedDataKeys.localizationSettings)?.get(LocalizationSettings.self) {
localizationSettings = current
} else {
localizationSettings = nil
}
if let localizationSettings = localizationSettings {
return (primary: (localizationSettings.primaryComponent.languageCode, localizationSettings.primaryComponent.localization.version, localizationSettings.primaryComponent.localization.entries), secondary: localizationSettings.secondaryComponent.flatMap({ ($0.languageCode, $0.localization.version, $0.localization.entries) }))
} else {
return (primary: ("en", 0, []), secondary: nil)
}
}
private func parseLangPackDifference(_ difference: Api.LangPackDifference) -> (code: String, fromVersion: Int32, version: Int32, entries: [LocalizationEntry]) {
switch difference {
case let .langPackDifference(code, fromVersion, version, strings):
var entries: [LocalizationEntry] = []
for string in strings {
switch string {
case let .langPackString(key, value):
entries.append(.string(key: key, value: value))
case let .langPackStringPluralized(_, key, zeroValue, oneValue, twoValue, fewValue, manyValue, otherValue):
entries.append(.pluralizedString(key: key, zero: zeroValue, one: oneValue, two: twoValue, few: fewValue, many: manyValue, other: otherValue))
case let .langPackStringDeleted(key):
entries.append(.string(key: key, value: ""))
}
}
return (code, fromVersion, version, entries)
}
}
private func synchronizeLocalizationUpdates(accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let currentLanguageAndVersion = accountManager.transaction { transaction -> (primary: (code: String, version: Int32), secondary: (code: String, version: Int32)?) in
let (primary, secondary) = getLocalization(transaction)
return ((primary.code, primary.version), secondary.flatMap({ ($0.code, $0.version) }))
}
let poll = currentLanguageAndVersion
|> castError(SynchronizeLocalizationUpdatesError.self)
|> mapToSignal { (primary, secondary) -> Signal<Void, SynchronizeLocalizationUpdatesError> in
var differences: [Signal<Api.LangPackDifference, MTRpcError>] = []
differences.append(network.request(Api.functions.langpack.getDifference(langPack: "", langCode: primary.code, fromVersion: primary.version)))
if let secondary = secondary {
differences.append(network.request(Api.functions.langpack.getDifference(langPack: "", langCode: secondary.code, fromVersion: secondary.version)))
}
return combineLatest(differences)
|> mapError { _ -> SynchronizeLocalizationUpdatesError in return .reset }
|> mapToSignal { differences -> Signal<Void, SynchronizeLocalizationUpdatesError> in
let parsedDifferences = differences.map(parseLangPackDifference)
return accountManager.transaction { transaction -> Signal<Void, SynchronizeLocalizationUpdatesError> in
let (primary, secondary) = getLocalization(transaction)
var currentSettings = transaction.getSharedData(SharedDataKeys.localizationSettings)?.get(LocalizationSettings.self) ?? LocalizationSettings(primaryComponent: LocalizationComponent(languageCode: "en", localizedName: "English", localization: Localization(version: 0, entries: []), customPluralizationCode: nil), secondaryComponent: nil)
for difference in parsedDifferences {
let current: (isPrimary: Bool, entries: [LocalizationEntry])
if difference.code == primary.code {
if primary.version != difference.fromVersion {
return .complete()
}
current = (true, primary.entries)
} else if let secondary = secondary, difference.code == secondary.code {
if secondary.version != difference.fromVersion {
return .complete()
}
current = (false, secondary.entries)
} else {
return .fail(.reset)
}
var updatedEntryKeys = Set<String>()
for entry in difference.entries {
updatedEntryKeys.insert(entry.key)
}
var mergedEntries: [LocalizationEntry] = []
for entry in current.entries {
if !updatedEntryKeys.contains(entry.key) {
mergedEntries.append(entry)
}
}
mergedEntries.append(contentsOf: difference.entries)
if current.isPrimary {
currentSettings = LocalizationSettings(primaryComponent: LocalizationComponent(languageCode: currentSettings.primaryComponent.languageCode, localizedName: currentSettings.primaryComponent.localizedName, localization: Localization(version: difference.version, entries: mergedEntries), customPluralizationCode: currentSettings.primaryComponent.customPluralizationCode), secondaryComponent: currentSettings.secondaryComponent)
} else if let currentSecondary = currentSettings.secondaryComponent {
currentSettings = LocalizationSettings(primaryComponent: currentSettings.primaryComponent, secondaryComponent: LocalizationComponent(languageCode: currentSecondary.languageCode, localizedName: currentSecondary.localizedName, localization: Localization(version: difference.version, entries: mergedEntries), customPluralizationCode: currentSecondary.customPluralizationCode))
}
}
transaction.updateSharedData(SharedDataKeys.localizationSettings, { _ in
return PreferencesEntry(currentSettings)
})
return .fail(.done)
}
|> mapError { _ -> SynchronizeLocalizationUpdatesError in
}
|> switchToLatest
}
}
return ((poll
|> `catch` { error -> Signal<Void, Void> in
switch error {
case .done:
return .fail(Void())
case .reset:
return accountManager.transaction { transaction -> Signal<Void, Void> in
let (primary, _) = getLocalization(transaction)
return _internal_downloadAndApplyLocalization(accountManager: accountManager, postbox: postbox, network: network, languageCode: primary.code)
|> mapError { _ -> Void in
return Void()
}
}
|> castError(Void.self)
|> switchToLatest
}
}) |> restart) |> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
}
func tryApplyingLanguageDifference(transaction: AccountManagerModifier<TelegramAccountManagerTypes>, langCode: String, difference: Api.LangPackDifference) -> Bool {
let (primary, secondary) = getLocalization(transaction)
switch difference {
case let .langPackDifference(updatedCode, fromVersion, updatedVersion, strings):
var current: (isPrimary: Bool, version: Int32, entries: [LocalizationEntry])?
if updatedCode == primary.code {
current = (true, primary.version, primary.entries)
} else if let secondary = secondary, secondary.code == updatedCode {
current = (false, secondary.version, secondary.entries)
}
guard let (isPrimary, version, entries) = current else {
return false
}
guard fromVersion == version else {
return false
}
var updatedEntries: [LocalizationEntry] = []
for string in strings {
switch string {
case let .langPackString(key, value):
updatedEntries.append(.string(key: key, value: value))
case let .langPackStringPluralized(_, key, zeroValue, oneValue, twoValue, fewValue, manyValue, otherValue):
updatedEntries.append(.pluralizedString(key: key, zero: zeroValue, one: oneValue, two: twoValue, few: fewValue, many: manyValue, other: otherValue))
case let .langPackStringDeleted(key):
updatedEntries.append(.string(key: key, value: ""))
}
}
var updatedEntryKeys = Set<String>()
for entry in updatedEntries {
updatedEntryKeys.insert(entry.key)
}
var mergedEntries: [LocalizationEntry] = []
for entry in entries {
if !updatedEntryKeys.contains(entry.key) {
mergedEntries.append(entry)
}
}
mergedEntries.append(contentsOf: updatedEntries)
let currentSettings = transaction.getSharedData(SharedDataKeys.localizationSettings)?.get(LocalizationSettings.self) ?? LocalizationSettings(primaryComponent: LocalizationComponent(languageCode: "en", localizedName: "English", localization: Localization(version: 0, entries: []), customPluralizationCode: nil), secondaryComponent: nil)
var updatedSettings: LocalizationSettings
if isPrimary {
updatedSettings = LocalizationSettings(primaryComponent: LocalizationComponent(languageCode: currentSettings.primaryComponent.languageCode, localizedName: currentSettings.primaryComponent.localizedName, localization: Localization(version: updatedVersion, entries: mergedEntries), customPluralizationCode: currentSettings.primaryComponent.customPluralizationCode), secondaryComponent: currentSettings.secondaryComponent)
} else if let currentSecondary = currentSettings.secondaryComponent {
updatedSettings = LocalizationSettings(primaryComponent: currentSettings.primaryComponent, secondaryComponent: LocalizationComponent(languageCode: currentSecondary.languageCode, localizedName: currentSecondary.localizedName, localization: Localization(version: updatedVersion, entries: mergedEntries), customPluralizationCode: currentSecondary.customPluralizationCode))
} else {
assertionFailure()
return false
}
transaction.updateSharedData(SharedDataKeys.localizationSettings, { _ in
return PreferencesEntry(updatedSettings)
})
return true
}
}
@@ -0,0 +1,237 @@
import Foundation
import Postbox
import SwiftSignalKit
private final class ManagedMessageHistoryHolesContext {
private struct LocationKey: Equatable {
var peerId: PeerId
var threadId: Int64?
var space: MessageHistoryHoleOperationSpace
init(peerId: PeerId, threadId: Int64?, space: MessageHistoryHoleOperationSpace) {
self.peerId = peerId
self.threadId = threadId
self.space = space
}
}
private struct PendingEntry: CustomStringConvertible {
var id: Int
var key: LocationKey
var entry: MessageHistoryHolesViewEntry
var disposable: MetaDisposable
init(id: Int, key: LocationKey, entry: MessageHistoryHolesViewEntry, disposable: MetaDisposable) {
self.id = id
self.key = key
self.entry = entry
self.disposable = disposable
}
var description: String {
return "entry: \(self.entry)"
}
}
private struct DiscardedEntry {
var entry: PendingEntry
var timestamp: Double
init(entry: PendingEntry, timestamp: Double) {
self.entry = entry
self.timestamp = timestamp
}
}
private let queue: Queue
private let accountPeerId: PeerId
private let postbox: Postbox
private let network: Network
private var nextEntryId: Int = 0
private var pendingEntries: [PendingEntry] = []
private var discardedEntries: [DiscardedEntry] = []
private var oldEntriesTimer: SwiftSignalKit.Timer?
private var currentEntries: Set<MessageHistoryHolesViewEntry> = Set()
private var currentEntriesDisposable: Disposable?
private var completedEntries: [MessageHistoryHolesViewEntry: Double] = [:]
init(
queue: Queue,
accountPeerId: PeerId,
postbox: Postbox,
network: Network,
entries: Signal<Set<MessageHistoryHolesViewEntry>, NoError>
) {
self.queue = queue
self.accountPeerId = accountPeerId
self.postbox = postbox
self.network = network
self.currentEntriesDisposable = (entries |> deliverOn(self.queue)).start(next: { [weak self] entries in
guard let self = self else {
return
}
self.update(entries: entries)
})
}
deinit {
assert(self.queue.isCurrent())
self.oldEntriesTimer?.invalidate()
self.currentEntriesDisposable?.dispose()
}
func resetPeer(peerId: PeerId) {
for entry in Array(self.completedEntries.keys) {
switch entry.hole {
case let .peer(peer):
if peer.peerId == peerId {
self.completedEntries.removeValue(forKey: entry)
}
}
}
}
func clearDisposables() -> [Disposable] {
var disposables = Array(self.pendingEntries.map(\.disposable))
disposables.append(contentsOf: self.discardedEntries.map(\.entry.disposable))
self.pendingEntries.removeAll()
self.discardedEntries.removeAll()
return disposables
}
private func updateNeedsTimer() {
let needsTimer = !self.discardedEntries.isEmpty
if needsTimer {
if self.oldEntriesTimer == nil {
self.oldEntriesTimer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in
guard let self = self else {
return
}
let disposables = self.discardOldEntries()
for disposable in disposables {
disposable.dispose()
}
}, queue: self.queue)
self.oldEntriesTimer?.start()
}
} else if let oldEntriesTimer = self.oldEntriesTimer {
self.oldEntriesTimer = nil
oldEntriesTimer.invalidate()
}
}
private func discardOldEntries() -> [Disposable] {
let timestamp = CFAbsoluteTimeGetCurrent()
var result: [Disposable] = []
for i in (0 ..< self.discardedEntries.count).reversed() {
if self.discardedEntries[i].timestamp < timestamp - 0.5 {
result.append(self.discardedEntries[i].entry.disposable)
Logger.shared.log("ManagedMessageHistoryHoles", "Removing discarded entry \(self.discardedEntries[i].entry)")
self.discardedEntries.remove(at: i)
}
}
return result
}
func update(entries: Set<MessageHistoryHolesViewEntry>) {
//let removed: [Disposable] = []
var added: [PendingEntry] = []
let timestamp = CFAbsoluteTimeGetCurrent()
let _ = timestamp
/*for i in (0 ..< self.pendingEntries.count).reversed() {
if !entries.contains(self.pendingEntries[i].entry) {
Logger.shared.log("ManagedMessageHistoryHoles", "Stashing entry \(self.pendingEntries[i])")
self.discardedEntries.append(DiscardedEntry(entry: self.pendingEntries[i], timestamp: timestamp))
self.pendingEntries.remove(at: i)
}
}*/
for entry in entries {
if let previousTimestamp = self.completedEntries[entry] {
if previousTimestamp >= CFAbsoluteTimeGetCurrent() - 20.0 {
Logger.shared.log("ManagedMessageHistoryHoles", "Not adding recently finished entry \(entry)")
continue
}
}
switch entry.hole {
case let .peer(peerHole):
let key = LocationKey(peerId: peerHole.peerId, threadId: peerHole.threadId, space: entry.space)
if !self.pendingEntries.contains(where: { $0.key == key }) {
if let discardedIndex = self.discardedEntries.firstIndex(where: { $0.entry.entry == entry }) {
let discardedEntry = self.discardedEntries.remove(at: discardedIndex)
Logger.shared.log("ManagedMessageHistoryHoles", "Taking discarded entry \(discardedEntry.entry)")
self.pendingEntries.append(discardedEntry.entry)
} else {
let disposable = MetaDisposable()
let id = self.nextEntryId
self.nextEntryId += 1
let pendingEntry = PendingEntry(id: id, key: key, entry: entry, disposable: disposable)
self.pendingEntries.append(pendingEntry)
Logger.shared.log("ManagedMessageHistoryHoles", "Adding pending entry \(pendingEntry), discarded entries: \(self.discardedEntries.map(\.entry))")
added.append(pendingEntry)
}
}
}
}
self.updateNeedsTimer()
for pendingEntry in added {
let id = pendingEntry.id
let entry = pendingEntry.entry
switch pendingEntry.entry.hole {
case let .peer(hole):
pendingEntry.disposable.set((fetchMessageHistoryHole(
accountPeerId: self.accountPeerId,
source: .network(self.network),
postbox: self.postbox,
peerInput: .direct(peerId: hole.peerId, threadId: hole.threadId), namespace: hole.namespace, direction: pendingEntry.entry.direction, space: pendingEntry.entry.space, count: pendingEntry.entry.count)
|> deliverOn(self.queue)).start(completed: { [weak self] in
guard let self = self else {
return
}
self.pendingEntries.removeAll(where: { $0.id == id })
self.completedEntries[entry] = CFAbsoluteTimeGetCurrent()
self.update(entries: self.currentEntries)
}))
}
}
}
}
func managedMessageHistoryHoles(accountPeerId: PeerId, network: Network, postbox: Postbox) -> ((PeerId) -> Void, Disposable) {
let sharedQueue = Queue()
var context: QueueLocalObject<ManagedMessageHistoryHolesContext>? = QueueLocalObject<ManagedMessageHistoryHolesContext>(queue: sharedQueue, generate: {
return ManagedMessageHistoryHolesContext(
queue: sharedQueue,
accountPeerId: accountPeerId,
postbox: postbox,
network: network,
entries: postbox.messageHistoryHolesView() |> map { view in
return view.entries
}
)
})
return ({ [weak context] peerId in
context?.with { context in
context.resetPeer(peerId: peerId)
}
}, ActionDisposable {
if context != nil {
context = nil
}
})
}
@@ -0,0 +1,35 @@
import Foundation
import Postbox
import SwiftSignalKit
func managedNotificationSettingsBehaviors(postbox: Postbox) -> Signal<Never, NoError> {
return postbox.combinedView(keys: [.peerNotificationSettingsBehaviorTimestampView])
|> mapToSignal { views -> Signal<Never, NoError> in
guard let view = views.views[.peerNotificationSettingsBehaviorTimestampView] as? PeerNotificationSettingsBehaviorTimestampView else {
return .complete()
}
guard let earliestTimestamp = view.earliestTimestamp else {
return .complete()
}
let checkSignal = postbox.transaction { transaction -> Void in
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
for (peerId, notificationSettings) in transaction.getPeerIdsAndNotificationSettingsWithBehaviorTimestampLessThanOrEqualTo(timestamp) {
if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings {
if case let .muted(untilTimestamp) = notificationSettings.muteState, untilTimestamp <= timestamp {
transaction.updateCurrentPeerNotificationSettings([peerId: notificationSettings.withUpdatedMuteState(.unmuted)])
}
}
}
}
|> ignoreValues
let timeout = earliestTimestamp - Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
if timeout <= 0 {
return checkSignal
} else {
return checkSignal |> delay(Double(timeout), queue: .mainQueue())
}
}
}
@@ -0,0 +1,415 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public final class EngineAvailableColorOptions: Codable, Equatable {
public final class MultiColorPack: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case colors = "c"
}
public let colors: [UInt32]
public init(colors: [UInt32]) {
self.colors = colors
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.colors = try container.decode([Int32].self, forKey: .colors).map(UInt32.init(bitPattern:))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.colors.map(Int32.init(bitPattern:)), forKey: .colors)
}
public static func ==(lhs: MultiColorPack, rhs: MultiColorPack) -> Bool {
if lhs === rhs {
return true
}
if lhs.colors != rhs.colors {
return false
}
return true
}
}
public final class ColorOption: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case palette = "p"
case background = "b"
case stories = "s"
}
public let palette: MultiColorPack
public let background: MultiColorPack
public let stories: MultiColorPack?
public init(palette: MultiColorPack, background: MultiColorPack, stories: MultiColorPack?) {
self.palette = palette
self.background = background
self.stories = stories
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.palette = try container.decode(MultiColorPack.self, forKey: .palette)
self.background = try container.decode(MultiColorPack.self, forKey: .background)
self.stories = try container.decodeIfPresent(MultiColorPack.self, forKey: .stories)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.palette, forKey: .palette)
try container.encode(self.background, forKey: .background)
try container.encodeIfPresent(self.stories, forKey: .stories)
}
public static func ==(lhs: ColorOption, rhs: ColorOption) -> Bool {
if lhs === rhs {
return true
}
if lhs.palette != rhs.palette {
return false
}
if lhs.background != rhs.background {
return false
}
if lhs.stories != rhs.stories {
return false
}
return true
}
}
public final class ColorOptionPack: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case light = "l"
case dark = "d"
case isHidden = "h"
case requiredChannelMinBoostLevel = "rcmb"
case requiredGroupMinBoostLevel = "rgmb"
}
public let light: ColorOption
public let dark: ColorOption?
public let isHidden: Bool
public let requiredChannelMinBoostLevel: Int32?
public let requiredGroupMinBoostLevel: Int32?
public init(light: ColorOption, dark: ColorOption?, isHidden: Bool, requiredChannelMinBoostLevel: Int32?, requiredGroupMinBoostLevel: Int32?) {
self.light = light
self.dark = dark
self.isHidden = isHidden
self.requiredChannelMinBoostLevel = requiredChannelMinBoostLevel
self.requiredGroupMinBoostLevel = requiredGroupMinBoostLevel
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.light = try container.decode(ColorOption.self, forKey: .light)
self.dark = try container.decodeIfPresent(ColorOption.self, forKey: .dark)
self.isHidden = try container.decode(Bool.self, forKey: .isHidden)
self.requiredChannelMinBoostLevel = try container.decodeIfPresent(Int32.self, forKey: .requiredChannelMinBoostLevel)
self.requiredGroupMinBoostLevel = try container.decodeIfPresent(Int32.self, forKey: .requiredGroupMinBoostLevel)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.light, forKey: .light)
try container.encodeIfPresent(self.dark, forKey: .dark)
try container.encodeIfPresent(self.isHidden, forKey: .isHidden)
try container.encodeIfPresent(self.requiredChannelMinBoostLevel, forKey: .requiredChannelMinBoostLevel)
try container.encodeIfPresent(self.requiredGroupMinBoostLevel, forKey: .requiredGroupMinBoostLevel)
}
public static func ==(lhs: ColorOptionPack, rhs: ColorOptionPack) -> Bool {
if lhs === rhs {
return true
}
if lhs.light != rhs.light {
return false
}
if lhs.dark != rhs.dark {
return false
}
if lhs.isHidden != rhs.isHidden {
return false
}
if lhs.requiredChannelMinBoostLevel != rhs.requiredChannelMinBoostLevel {
return false
}
if lhs.requiredGroupMinBoostLevel != rhs.requiredGroupMinBoostLevel {
return false
}
return true
}
}
private enum CodingKeys: String, CodingKey {
case hash = "h"
case options = "o"
}
public final class Option: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case key = "k"
case value = "v"
}
public let key: Int32
public let value: ColorOptionPack
public init(key: Int32, value: ColorOptionPack) {
self.key = key
self.value = value
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.key = try container.decode(Int32.self, forKey: .key)
self.value = try container.decode(ColorOptionPack.self, forKey: .value)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.key, forKey: .key)
try container.encode(self.value, forKey: .value)
}
public static func ==(lhs: Option, rhs: Option) -> Bool {
if lhs === rhs {
return true
}
if lhs.key != rhs.key {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
}
public let hash: Int32
public let options: [Option]
public init(hash: Int32, options: [Option]) {
self.hash = hash
self.options = options
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.hash = try container.decode(Int32.self, forKey: .hash)
self.options = try container.decode([Option].self, forKey: .options)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.hash, forKey: .hash)
try container.encode(self.options, forKey: .options)
}
public static func ==(lhs: EngineAvailableColorOptions, rhs: EngineAvailableColorOptions) -> Bool {
if lhs === rhs {
return true
}
if lhs.hash != rhs.hash {
return false
}
if lhs.options != rhs.options {
return false
}
return true
}
}
private extension EngineAvailableColorOptions.ColorOption {
convenience init?(apiColors: Api.help.PeerColorSet) {
let palette: EngineAvailableColorOptions.MultiColorPack
let background: EngineAvailableColorOptions.MultiColorPack
let stories: EngineAvailableColorOptions.MultiColorPack?
switch apiColors {
case let .peerColorSet(colors):
if colors.isEmpty {
return nil
}
palette = EngineAvailableColorOptions.MultiColorPack(colors: colors.map(UInt32.init(bitPattern:)))
background = palette
stories = nil
case let .peerColorProfileSet(palleteColors, bgColors, storyColors):
if palleteColors.isEmpty {
return nil
}
palette = EngineAvailableColorOptions.MultiColorPack(colors: palleteColors.map(UInt32.init(bitPattern:)))
if bgColors.isEmpty {
return nil
}
background = EngineAvailableColorOptions.MultiColorPack(colors: bgColors.map(UInt32.init(bitPattern:)))
if !storyColors.isEmpty {
stories = EngineAvailableColorOptions.MultiColorPack(colors: storyColors.map(UInt32.init(bitPattern:)))
} else {
stories = nil
}
}
self.init(palette: palette, background: background, stories: stories)
}
}
private extension EngineAvailableColorOptions {
convenience init(hash: Int32, apiColors: [Api.help.PeerColorOption]) {
var mappedOptions: [Option] = []
for apiColor in apiColors {
switch apiColor {
case let .peerColorOption(flags, colorId, colors, darkColors, requiredChannelMinBoostLevel, requiredGroupMinBoostLevel):
let isHidden = (flags & (1 << 0)) != 0
let mappedColors = colors.flatMap(EngineAvailableColorOptions.ColorOption.init(apiColors:))
let mappedDarkColors = darkColors.flatMap(EngineAvailableColorOptions.ColorOption.init(apiColors:))
if let mappedColors = mappedColors {
mappedOptions.append(Option(key: colorId, value: ColorOptionPack(light: mappedColors, dark: mappedDarkColors, isHidden: isHidden, requiredChannelMinBoostLevel: requiredChannelMinBoostLevel, requiredGroupMinBoostLevel: requiredGroupMinBoostLevel)))
} else if colorId >= 0 && colorId <= 6 {
let staticMap: [UInt32] = [
0xcc5049,
0xd67722,
0x955cdb,
0x40a920,
0x309eba,
0x368ad1,
0xc7508b
]
let colorPack = MultiColorPack(colors: [staticMap[Int(colorId)]])
let defaultColors = EngineAvailableColorOptions.ColorOption(palette: colorPack, background: colorPack, stories: nil)
mappedOptions.append(Option(key: colorId, value: ColorOptionPack(light: defaultColors, dark: nil, isHidden: isHidden, requiredChannelMinBoostLevel: requiredChannelMinBoostLevel, requiredGroupMinBoostLevel: requiredGroupMinBoostLevel)))
}
}
}
self.init(hash: hash, options: mappedOptions)
}
}
func managedPeerColorUpdates(postbox: Postbox, network: Network) -> Signal<Never, NoError> {
let poll = combineLatest(
_internal_fetchPeerColors(postbox: postbox, network: network, scope: .replies),
_internal_fetchPeerColors(postbox: postbox, network: network, scope: .profile)
)
|> mapToSignal { _ -> Signal<Never, NoError> in
}
return (poll |> then(.complete() |> suspendAwareDelay(2.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
public enum PeerColorsScope {
case replies
case profile
}
private func _internal_fetchPeerColors(postbox: Postbox, network: Network, scope: PeerColorsScope) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Int32 in
#if DEBUG
if "".isEmpty {
return 0
}
#endif
return _internal_cachedAvailableColorOptions(transaction: transaction, scope: scope).hash
}
|> mapToSignal { hash -> Signal<Never, NoError> in
let signal: Signal<Api.help.PeerColors, MTRpcError>
switch scope {
case .replies:
signal = network.request(Api.functions.help.getPeerColors(hash: hash))
case .profile:
signal = network.request(Api.functions.help.getPeerProfileColors(hash: hash))
}
return signal
|> `catch` { _ -> Signal<Api.help.PeerColors, NoError> in
return .single(.peerColorsNotModified)
}
|> mapToSignal { result -> Signal<Never, NoError> in
switch result {
case .peerColorsNotModified:
return .complete()
case let .peerColors(hash, colors):
return postbox.transaction { transaction -> Void in
let value = EngineAvailableColorOptions(hash: hash, apiColors: colors)
_internal_setCachedAvailableColorOptions(transaction: transaction, scope: scope, value: value)
}
|> ignoreValues
}
}
}
}
func _internal_cachedAvailableColorOptions(postbox: Postbox, scope: PeerColorsScope) -> Signal<EngineAvailableColorOptions, NoError> {
return postbox.transaction { transaction -> EngineAvailableColorOptions in
return _internal_cachedAvailableColorOptions(transaction: transaction, scope: scope)
}
}
func _internal_observeAvailableColorOptions(postbox: Postbox, scope: PeerColorsScope) -> Signal<EngineAvailableColorOptions, NoError> {
let key = ValueBoxKey(length: 8)
switch scope {
case .replies:
key.setInt64(0, value: 0)
case .profile:
key.setInt64(0, value: 1)
}
let viewKey: PostboxViewKey = .cachedItem(ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.peerColorOptions, key: key))
return postbox.combinedView(keys: [viewKey])
|> map { views -> EngineAvailableColorOptions in
guard let view = views.views[viewKey] as? CachedItemView, let value = view.value?.get(EngineAvailableColorOptions.self) else {
return EngineAvailableColorOptions(hash: 0, options: [])
}
return value
}
}
func _internal_cachedAvailableColorOptions(transaction: Transaction, scope: PeerColorsScope) -> EngineAvailableColorOptions {
let key = ValueBoxKey(length: 8)
switch scope {
case .replies:
key.setInt64(0, value: 0)
case .profile:
key.setInt64(0, value: 1)
}
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.peerColorOptions, key: key))?.get(EngineAvailableColorOptions.self)
if let cached = cached {
return cached
} else {
return EngineAvailableColorOptions(hash: 0, options: [])
}
}
func _internal_setCachedAvailableColorOptions(transaction: Transaction, scope: PeerColorsScope, value: EngineAvailableColorOptions) {
let key = ValueBoxKey(length: 8)
switch scope {
case .replies:
key.setInt64(0, value: 0)
case .profile:
key.setInt64(0, value: 1)
}
if let entry = CodableEntry(value) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.peerColorOptions, key: key), entry: entry)
}
}
@@ -0,0 +1,110 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private typealias SignalKitTimer = SwiftSignalKit.Timer
private final class ManagedPeerTimestampAttributeOperationsHelper {
struct Entry: Equatable {
var peerId: PeerId
var timestamp: UInt32
}
var entry: (Entry, MetaDisposable)?
func update(_ head: Entry?) -> (disposeOperations: [Disposable], beginOperations: [(Entry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(Entry, MetaDisposable)] = []
if self.entry?.0 != head {
if let (_, disposable) = self.entry {
self.entry = nil
disposeOperations.append(disposable)
}
if let head = head {
let disposable = MetaDisposable()
self.entry = (head, disposable)
beginOperations.append((head, disposable))
}
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
if let entry = entry {
return [entry.1]
} else {
return []
}
}
}
func managedPeerTimestampAttributeOperations(network: Network, postbox: Postbox) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic(value: ManagedPeerTimestampAttributeOperationsHelper())
let timeOffsetOnce = Signal<Double, NoError> { subscriber in
subscriber.putNext(network.globalTimeDifference)
return EmptyDisposable
}
let timeOffset = (
timeOffsetOnce
|> then(
Signal<Double, NoError>.complete()
|> delay(1.0, queue: .mainQueue())
)
)
|> restart
|> map { value -> Double in
round(value)
}
|> distinctUntilChanged
let disposable = combineLatest(timeOffset, postbox.combinedView(keys: [PostboxViewKey.peerTimeoutAttributes])).start(next: { timeOffset, views in
guard let view = views.views[PostboxViewKey.peerTimeoutAttributes] as? PeerTimeoutAttributesView else {
return
}
let topEntry = view.minValue.flatMap { value in
return ManagedPeerTimestampAttributeOperationsHelper.Entry(peerId: value.peerId, timestamp: value.timestamp)
}
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(ManagedPeerTimestampAttributeOperationsHelper.Entry, MetaDisposable)]) in
return helper.update(topEntry)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + timeOffset
let delay = max(0.0, Double(entry.timestamp) - timestamp)
let signal = Signal<Void, NoError>.complete()
|> suspendAwareDelay(delay, queue: Queue.concurrentDefaultQueue())
|> then(postbox.transaction { transaction -> Void in
if let peer = transaction.getPeer(entry.peerId) {
if let user = peer as? TelegramUser {
updatePeersCustom(transaction: transaction, peers: [user.withUpdatedEmojiStatus(nil)], update: { _, updated in updated })
}
}
//failsafe
transaction.removePeerTimeoutAttributeEntry(peerId: entry.peerId, timestamp: entry.timestamp)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
disposable.dispose()
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
}
}
}
@@ -0,0 +1,267 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedPendingPeerNotificationSettingsHelper {
var operationDisposables: [PeerId: (PeerNotificationSettings, Disposable)] = [:]
func update(entries: [PeerId: PeerNotificationSettings]) -> (disposeOperations: [Disposable], beginOperations: [(PeerId, PeerNotificationSettings, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerId, PeerNotificationSettings, MetaDisposable)] = []
var validIds = Set<PeerId>()
for (peerId, settings) in entries {
validIds.insert(peerId)
if let (currentSettings, currentDisposable) = self.operationDisposables[peerId] {
if !currentSettings.isEqual(to: settings) {
disposeOperations.append(currentDisposable)
let disposable = MetaDisposable()
beginOperations.append((peerId, settings, disposable))
self.operationDisposables[peerId] = (settings, disposable)
}
} else {
let disposable = MetaDisposable()
beginOperations.append((peerId, settings, disposable))
self.operationDisposables[peerId] = (settings, disposable)
}
}
var removeIds: [PeerId] = []
for (id, settingsAndDisposable) in self.operationDisposables {
if !validIds.contains(id) {
removeIds.append(id)
disposeOperations.append(settingsAndDisposable.1)
}
}
for id in removeIds {
self.operationDisposables.removeValue(forKey: id)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values).map { $0.1 }
self.operationDisposables.removeAll()
return disposables
}
}
func managedPendingPeerNotificationSettings(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedPendingPeerNotificationSettingsHelper>(value: ManagedPendingPeerNotificationSettingsHelper())
let disposable = postbox.combinedView(keys: [.pendingPeerNotificationSettings]).start(next: { view in
var entries: [PeerId: PeerNotificationSettings] = [:]
if let v = view.views[.pendingPeerNotificationSettings] as? PendingPeerNotificationSettingsView {
entries = v.entries
}
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerId, PeerNotificationSettings, MetaDisposable)]) in
return helper.update(entries: entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (peerId, settings, disposable) in beginOperations {
let signal = pushPeerNotificationSettings(postbox: postbox, network: network, peerId: peerId, threadId: nil, settings: settings)
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
func pushPeerNotificationSettings(postbox: Postbox, network: Network, peerId: PeerId, threadId: Int64?, settings: PeerNotificationSettings) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
var notificationPeerId = peerId
if peer is TelegramSecretChat, let associatedPeerId = peer.associatedPeerId {
notificationPeerId = associatedPeerId
}
if let threadId = threadId {
if let data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) {
let settings = data.notificationSettings
let showPreviews: Api.Bool?
switch settings.displayPreviews {
case .default:
showPreviews = nil
case .show:
showPreviews = .boolTrue
case .hide:
showPreviews = .boolFalse
}
let muteUntil: Int32?
switch settings.muteState {
case let .muted(until):
muteUntil = until
case .unmuted:
muteUntil = 0
case .default:
muteUntil = nil
}
let sound: Api.NotificationSound? = settings.messageSound.apiSound
var flags: Int32 = 0
if showPreviews != nil {
flags |= (1 << 0)
}
if muteUntil != nil {
flags |= (1 << 2)
}
if sound != nil {
flags |= (1 << 3)
}
let storiesMuted: Api.Bool?
switch settings.storySettings.mute {
case .default:
storiesMuted = nil
case .muted:
storiesMuted = .boolTrue
case .unmuted:
storiesMuted = .boolFalse
}
if storiesMuted != nil {
flags |= (1 << 6)
}
let storiesHideSender: Api.Bool?
switch settings.storySettings.hideSender {
case .default:
storiesHideSender = nil
case .hide:
storiesHideSender = .boolTrue
case .show:
storiesHideSender = .boolFalse
}
if storiesHideSender != nil {
flags |= (1 << 7)
}
let storiesSound: Api.NotificationSound? = settings.storySettings.sound.apiSound
if storiesSound != nil {
flags |= (1 << 8)
}
let inputSettings = Api.InputPeerNotifySettings.inputPeerNotifySettings(flags: flags, showPreviews: showPreviews, silent: nil, muteUntil: muteUntil, sound: sound, storiesMuted: storiesMuted, storiesHideSender: storiesHideSender, storiesSound: storiesSound)
return network.request(Api.functions.account.updateNotifySettings(peer: .inputNotifyForumTopic(peer: inputPeer, topMsgId: Int32(clamping: threadId)), settings: inputSettings))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
}
}
} else {
return .complete()
}
} else {
if let notificationPeer = transaction.getPeer(notificationPeerId), let inputPeer = apiInputPeer(notificationPeer), let settings = settings as? TelegramPeerNotificationSettings {
let showPreviews: Api.Bool?
switch settings.displayPreviews {
case .default:
showPreviews = nil
case .show:
showPreviews = .boolTrue
case .hide:
showPreviews = .boolFalse
}
let muteUntil: Int32?
switch settings.muteState {
case let .muted(until):
muteUntil = until
case .unmuted:
muteUntil = 0
case .default:
muteUntil = nil
}
let sound: Api.NotificationSound? = settings.messageSound.apiSound
var flags: Int32 = 0
if showPreviews != nil {
flags |= (1 << 0)
}
if muteUntil != nil {
flags |= (1 << 2)
}
if sound != nil {
flags |= (1 << 3)
}
let storiesMuted: Api.Bool?
switch settings.storySettings.mute {
case .default:
storiesMuted = nil
case .muted:
storiesMuted = .boolTrue
case .unmuted:
storiesMuted = .boolFalse
}
if storiesMuted != nil {
flags |= (1 << 6)
}
let storiesHideSender: Api.Bool?
switch settings.storySettings.hideSender {
case .default:
storiesHideSender = nil
case .hide:
storiesHideSender = .boolTrue
case .show:
storiesHideSender = .boolFalse
}
if storiesHideSender != nil {
flags |= (1 << 7)
}
let storiesSound: Api.NotificationSound? = settings.storySettings.sound.apiSound
if storiesSound != nil {
flags |= (1 << 8)
}
let inputSettings = Api.InputPeerNotifySettings.inputPeerNotifySettings(flags: flags, showPreviews: showPreviews, silent: nil, muteUntil: muteUntil, sound: sound, storiesMuted: storiesMuted, storiesHideSender: storiesHideSender, storiesSound: storiesSound)
return network.request(Api.functions.account.updateNotifySettings(peer: .inputNotifyPeer(peer: inputPeer), settings: inputSettings))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
transaction.updateCurrentPeerNotificationSettings([notificationPeerId: settings])
if let pending = transaction.getPendingPeerNotificationSettings(peerId), pending.isEqual(to: settings) {
transaction.updatePendingPeerNotificationSettings(peerId: peerId, settings: nil)
}
}
}
} else {
if let pending = transaction.getPendingPeerNotificationSettings(peerId), pending.isEqual(to: settings) {
transaction.updatePendingPeerNotificationSettings(peerId: peerId, settings: nil)
}
return .complete()
}
}
} else {
if let pending = transaction.getPendingPeerNotificationSettings(peerId), pending.isEqual(to: settings) {
transaction.updatePendingPeerNotificationSettings(peerId: peerId, settings: nil)
}
return .complete()
}
} |> switchToLatest
}
@@ -0,0 +1,83 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public func updatePremiumPromoConfigurationOnce(account: Account) -> Signal<Void, NoError> {
return updatePremiumPromoConfigurationOnce(accountPeerId: account.peerId, postbox: account.postbox, network: account.network)
}
func updatePremiumPromoConfigurationOnce(accountPeerId: PeerId, postbox: Postbox, network: Network) -> Signal<Void, NoError> {
return network.request(Api.functions.help.getPremiumPromo())
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.help.PremiumPromo?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
guard let result = result else {
return .complete()
}
return postbox.transaction { transaction -> Void in
if case let .premiumPromo(_, _, _, _, _, apiUsers) = result {
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: [], users: apiUsers)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
}
updatePremiumPromoConfiguration(transaction: transaction, { configuration -> PremiumPromoConfiguration in
return PremiumPromoConfiguration(apiPremiumPromo: result)
})
}
}
}
func managedPremiumPromoConfigurationUpdates(accountPeerId: PeerId, postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = Signal<Void, NoError> { subscriber in
return updatePremiumPromoConfigurationOnce(accountPeerId: accountPeerId, postbox: postbox, network: network).start(completed: {
subscriber.putCompletion()
})
}
return (poll |> then(.complete() |> suspendAwareDelay(10.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
private func currentPremiumPromoConfiguration(transaction: Transaction) -> PremiumPromoConfiguration {
if let entry = transaction.getPreferencesEntry(key: PreferencesKeys.premiumPromo)?.get(PremiumPromoConfiguration.self) {
return entry
} else {
return PremiumPromoConfiguration.defaultValue
}
}
private func updatePremiumPromoConfiguration(transaction: Transaction, _ f: (PremiumPromoConfiguration) -> PremiumPromoConfiguration) {
let current = currentPremiumPromoConfiguration(transaction: transaction)
let updated = f(current)
if updated != current {
transaction.setPreferencesEntry(key: PreferencesKeys.premiumPromo, value: PreferencesEntry(updated))
}
}
private extension PremiumPromoConfiguration {
init(apiPremiumPromo: Api.help.PremiumPromo) {
switch apiPremiumPromo {
case let .premiumPromo(statusText, statusEntities, videoSections, videoFiles, options, _):
self.status = statusText
self.statusEntities = messageTextEntitiesFromApiEntities(statusEntities)
var videos: [String: TelegramMediaFile] = [:]
for (key, document) in zip(videoSections, videoFiles) {
if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) {
videos[key] = file
}
}
self.videos = videos
var productOptions: [PremiumProductOption] = []
for option in options {
if case let .premiumSubscriptionOption(flags, transaction, months, currency, amount, botUrl, storeProduct) = option {
productOptions.append(PremiumProductOption(isCurrent: (flags & (1 << 1)) != 0, months: months, currency: currency, amount: amount, botUrl: botUrl, transactionId: transaction, availableForUpgrade: (flags & (1 << 2)) != 0, storeProductId: storeProduct))
}
}
self.premiumProductOptions = productOptions
}
}
}
@@ -0,0 +1,314 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
public final class PromoChatListItem: AdditionalChatListItem {
public enum Kind: Equatable {
case proxy
case psa(type: String, message: String?)
}
public let peerId: PeerId
public let kind: Kind
public var includeIfNoHistory: Bool {
switch self.kind {
case .proxy:
return false
case let .psa(_, message):
return message != nil
}
}
public init(peerId: PeerId, kind: Kind) {
self.peerId = peerId
self.kind = kind
}
public init(decoder: PostboxDecoder) {
self.peerId = PeerId(decoder.decodeInt64ForKey("peerId", orElse: 0))
let kindType = decoder.decodeInt32ForKey("_kind", orElse: 0)
switch kindType {
case 0:
self.kind = .proxy
case 1:
self.kind = .psa(type: decoder.decodeStringForKey("psa.type", orElse: "generic"), message: decoder.decodeOptionalStringForKey("psa.message"))
default:
assertionFailure()
self.kind = .proxy
}
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64(self.peerId.toInt64(), forKey: "peerId")
switch self.kind {
case .proxy:
encoder.encodeInt32(0, forKey: "_kind")
case let .psa(type, message):
encoder.encodeInt32(1, forKey: "_kind")
encoder.encodeString(type, forKey: "psa.type")
if let message = message {
encoder.encodeString(message, forKey: "psa.message")
} else {
encoder.encodeNil(forKey: "psa.message")
}
}
}
public func isEqual(to other: AdditionalChatListItem) -> Bool {
guard let other = other as? PromoChatListItem else {
return false
}
if self.peerId != other.peerId {
return false
}
if self.kind != other.kind {
return false
}
return true
}
}
public final class ServerSuggestionInfo: Codable, Equatable {
public final class Item: Codable, Equatable {
public final class Text: Codable, Equatable {
public let string: String
public let entities: [MessageTextEntity]
public init(string: String, entities: [MessageTextEntity]) {
self.string = string
self.entities = entities
}
public static func ==(lhs: Text, rhs: Text) -> Bool {
if lhs.string != rhs.string {
return false
}
if lhs.entities != rhs.entities {
return false
}
return true
}
}
public enum Action: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case link
}
case link(url: String)
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self = .link(url: try container.decode(String.self, forKey: .link))
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .link(url):
try container.encode(url, forKey: .link)
}
}
}
public let id: String
public let title: Text
public let text: Text
public let action: Action
public init(id: String, title: Text, text: Text, action: Action) {
self.id = id
self.title = title
self.text = text
self.action = action
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.action != rhs.action {
return false
}
return true
}
}
public let legacyItems: [String]
public let items: [Item]
public let dismissedIds: [String]
public init(legacyItems: [String], items: [Item], dismissedIds: [String]) {
self.legacyItems = legacyItems
self.items = items
self.dismissedIds = dismissedIds
}
public static func ==(lhs: ServerSuggestionInfo, rhs: ServerSuggestionInfo) -> Bool {
if lhs.items != rhs.items {
return false
}
return true
}
}
extension ServerSuggestionInfo.Item.Text {
convenience init(_ apiText: Api.TextWithEntities) {
switch apiText {
case let .textWithEntities(text, entities):
self.init(string: text, entities: messageTextEntitiesFromApiEntities(entities))
}
}
}
extension ServerSuggestionInfo.Item {
convenience init(_ apiItem: Api.PendingSuggestion) {
switch apiItem {
case let .pendingSuggestion(suggestion, title, description, url):
self.init(
id: suggestion,
title: ServerSuggestionInfo.Item.Text(title),
text: ServerSuggestionInfo.Item.Text(description),
action: .link(url: url)
)
}
}
}
func _internal_fetchPromoInfo(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network) -> Signal<Void, NoError> {
return network.request(Api.functions.help.getPromoData())
|> `catch` { _ -> Signal<Api.help.PromoData, NoError> in
return .single(.promoDataEmpty(expires: 10 * 60))
}
|> mapToSignal { data -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
switch data {
case .promoDataEmpty:
transaction.replaceAdditionalChatListItems([])
let suggestionInfo = ServerSuggestionInfo(
legacyItems: [],
items: [],
dismissedIds: []
)
transaction.updatePreferencesEntry(key: PreferencesKeys.serverSuggestionInfo(), { _ in
return PreferencesEntry(suggestionInfo)
})
case let .promoData(flags, expires, peer, psaType, psaMessage, pendingSuggestions, dismissedSuggestions, customPendingSuggestion, chats, users):
let _ = expires
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var kind: PromoChatListItem.Kind?
if let psaType {
kind = .psa(type: psaType, message: psaMessage)
} else if ((flags & 1) << 0) != 0 {
kind = .proxy
}
var additionalChatListItems: [AdditionalChatListItem] = []
if let kind, let peer, let parsedPeer = transaction.getPeer(peer.peerId) {
additionalChatListItems.append(PromoChatListItem(peerId: parsedPeer.id, kind: kind))
}
transaction.replaceAdditionalChatListItems(additionalChatListItems)
var customItems: [ServerSuggestionInfo.Item] = []
if let customPendingSuggestion {
customItems.append(ServerSuggestionInfo.Item(customPendingSuggestion))
}
let suggestionInfo = ServerSuggestionInfo(
legacyItems: pendingSuggestions,
items: customItems,
dismissedIds: dismissedSuggestions
)
transaction.updatePreferencesEntry(key: PreferencesKeys.serverSuggestionInfo(), { _ in
return PreferencesEntry(suggestionInfo)
})
}
}
}
}
func managedPromoInfoUpdates(accountPeerId: PeerId, postbox: Postbox, network: Network, viewTracker: AccountViewTracker) -> Signal<Void, NoError> {
return Signal { subscriber in
let queue = Queue()
let update = network.contextProxyId
|> distinctUntilChanged
|> deliverOn(queue)
|> mapToSignal { _ -> Signal<Void, NoError> in
return (_internal_fetchPromoInfo(accountPeerId: accountPeerId, postbox: postbox, network: network)
|> then(
Signal<Void, NoError>.complete()
|> delay(10.0 * 60.0, queue: Queue.concurrentDefaultQueue()))
)
|> restart
}
let updateDisposable = update.start()
let poll = postbox.combinedView(keys: [.additionalChatListItems])
|> map { views -> Set<PeerId> in
if let view = views.views[.additionalChatListItems] as? AdditionalChatListItemsView {
return Set(view.items.map { $0.peerId })
}
return Set()
}
|> distinctUntilChanged
|> mapToSignal { items -> Signal<Void, NoError> in
return Signal { subscriber in
let disposables = DisposableSet()
for item in items {
if item.namespace == Namespaces.Peer.CloudChannel {
disposables.add(viewTracker.polledChannel(peerId: item).start())
}
}
return ActionDisposable {
disposables.dispose()
}
}
}
let pollDisposable = poll.start()
return ActionDisposable {
updateDisposable.dispose()
pollDisposable.dispose()
}
}
}
public func hideAccountPromoInfoChat(account: Account, peerId: PeerId) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapToSignal { inputPeer -> Signal<Never, NoError> in
guard let inputPeer = inputPeer else {
return .complete()
}
return account.network.request(Api.functions.help.hidePromoData(peer: inputPeer))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { result -> Signal<Never, NoError> in
return account.postbox.transaction { transaction -> Void in
transaction.replaceAdditionalChatListItems([])
}
|> ignoreValues
}
}
}
@@ -0,0 +1,689 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
import MtProtoKit
private func hashForIds(_ ids: [Int64]) -> Int64 {
var acc: UInt64 = 0
for id in ids {
combineInt64Hash(&acc, with: UInt64(bitPattern: id))
}
return finalizeInt64Hash(acc)
}
private func managedRecentMedia(postbox: Postbox, network: Network, collectionId: Int32, extractItemId: @escaping (MemoryBuffer) -> Int64?, reverseHashOrder: Bool, forceFetch: Bool, fetch: @escaping (Int64) -> Signal<[OrderedItemListEntry]?, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var itemIds = transaction.getOrderedListItemIds(collectionId: collectionId).compactMap(extractItemId)
if reverseHashOrder {
itemIds.reverse()
}
return fetch(forceFetch ? 0 : hashForIds(itemIds))
|> mapToSignal { sourceItems in
var items: [OrderedItemListEntry] = []
if let sourceItems = sourceItems {
var existingIds = Set<Data>()
for item in sourceItems {
let id = item.id.makeData()
if !existingIds.contains(id) {
existingIds.insert(id)
items.append(item)
}
}
return postbox.transaction { transaction -> Void in
transaction.replaceOrderedItemListItems(collectionId: collectionId, items: items)
}
} else {
return .complete()
}
}
} |> switchToLatest
}
func managedRecentStickers(postbox: Postbox, network: Network, forceFetch: Bool = false) -> Signal<Void, NoError> {
return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: forceFetch, fetch: { hash in
return network.request(Api.functions.messages.getRecentStickers(flags: 0, hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .recentStickersNotModified:
return .single(nil)
case let .recentStickers(_, _, stickers, _):
var items: [OrderedItemListEntry] = []
for sticker in stickers {
if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id {
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry))
}
}
}
return .single(items)
}
}
})
}
func managedRecentGifs(postbox: Postbox, network: Network, forceFetch: Bool = false) -> Signal<Void, NoError> {
return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentGifs, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: forceFetch, fetch: { hash in
return network.request(Api.functions.messages.getSavedGifs(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .savedGifsNotModified:
return .single(nil)
case let .savedGifs(_, gifs):
var items: [OrderedItemListEntry] = []
for gif in gifs {
if let file = telegramMediaFileFromApiDocument(gif, altDocuments: []), let id = file.id {
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry))
}
}
}
return .single(items)
}
}
})
}
func managedSavedStickers(postbox: Postbox, network: Network, forceFetch: Bool = false) -> Signal<Void, NoError> {
return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudSavedStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: true, forceFetch: forceFetch, fetch: { hash in
return network.request(Api.functions.messages.getFavedStickers(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .favedStickersNotModified:
return .single(nil)
case let .favedStickers(_, packs, stickers):
var fileStringRepresentations: [MediaId: [String]] = [:]
for pack in packs {
switch pack {
case let .stickerPack(text, fileIds):
for fileId in fileIds {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if fileStringRepresentations[mediaId] == nil {
fileStringRepresentations[mediaId] = [text]
} else {
fileStringRepresentations[mediaId]!.append(text)
}
}
}
}
var items: [OrderedItemListEntry] = []
for sticker in stickers {
if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id {
var stringRepresentations: [String] = []
if let representations = fileStringRepresentations[id] {
stringRepresentations = representations
}
if let entry = CodableEntry(SavedStickerItem(file: file, stringRepresentations: stringRepresentations)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry))
}
}
}
return .single(items)
}
}
})
}
func managedGreetingStickers(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudGreetingStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.messages.getStickers(emoticon: "👋⭐️", hash: 0))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .stickersNotModified:
return .single(nil)
case let .stickers(_, stickers):
var items: [OrderedItemListEntry] = []
for sticker in stickers {
if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id {
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry))
}
}
}
return .single(items)
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedPremiumStickers(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudPremiumStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.messages.getStickers(emoticon: "⭐️⭐️", hash: 0))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .stickersNotModified:
return .single(nil)
case let .stickers(_, stickers):
var items: [OrderedItemListEntry] = []
for sticker in stickers {
if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id {
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry))
}
}
}
return .single(items)
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedAllPremiumStickers(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudAllPremiumStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.messages.getStickers(emoticon: "📂⭐️", hash: 0))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .stickersNotModified:
return .single(nil)
case let .stickers(_, stickers):
var items: [OrderedItemListEntry] = []
for sticker in stickers {
if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let id = file.id {
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry))
}
}
}
return .single(items)
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedRecentStatusEmoji(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentStatusEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.account.getRecentEmojiStatuses(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .emojiStatusesNotModified:
return .single(nil)
case let .emojiStatuses(_, statuses):
let parsedStatuses = statuses.compactMap(PeerEmojiStatus.init(apiStatus:))
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedStatuses.compactMap(\.emojiFileId))
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for status in parsedStatuses {
guard let fileId = status.emojiFileId, let file = files[fileId] else {
continue
}
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry))
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedFeaturedStatusEmoji(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedStatusEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.account.getDefaultEmojiStatuses(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .emojiStatusesNotModified:
return .single(nil)
case let .emojiStatuses(_, statuses):
let parsedStatuses = statuses.compactMap(PeerEmojiStatus.init(apiStatus:))
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedStatuses.compactMap(\.emojiFileId))
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for status in parsedStatuses {
guard let fileId = status.emojiFileId, let file = files[fileId] else {
continue
}
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry))
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedFeaturedChannelStatusEmoji(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedChannelStatusEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.account.getChannelDefaultEmojiStatuses(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .emojiStatusesNotModified:
return .single(nil)
case let .emojiStatuses(_, statuses):
let parsedStatuses = statuses.compactMap(PeerEmojiStatus.init(apiStatus:))
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedStatuses.compactMap(\.emojiFileId))
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for status in parsedStatuses {
guard let fileId = status.emojiFileId, let file = files[fileId] else {
continue
}
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry))
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudUniqueStarGifts, extractItemId: { RecentStarGiftItemId($0).id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.account.getCollectibleEmojiStatuses(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .emojiStatusesNotModified:
return .single(nil)
case let .emojiStatuses(_, statuses):
let parsedStatuses = statuses.compactMap(PeerEmojiStatus.init(apiStatus:))
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedStatuses.flatMap(\.associatedFileIds))
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for status in parsedStatuses {
switch status.content {
case let .starGift(id, fileId, title, slug, patternFileId, innerColor, outerColor, patternColor, textColor):
let slugComponents = slug.components(separatedBy: "-")
if let file = files[fileId], let patternFile = files[patternFileId], let numberString = slugComponents.last, let number = Int32(numberString) {
let gift = StarGift.UniqueGift(
id: id,
giftId: 0,
title: title,
number: number,
slug: slug,
owner: .peerId(accountPeerId),
attributes: [
.model(name: "", file: file, rarity: 0),
.pattern(name: "", file: patternFile, rarity: 0),
.backdrop(name: "", id: 0, innerColor: innerColor, outerColor: outerColor, patternColor: patternColor, textColor: textColor, rarity: 0)
],
availability: StarGift.UniqueGift.Availability(issued: 0, total: 0),
giftAddress: nil,
resellAmounts: nil,
resellForTonOnly: false,
releasedBy: nil,
valueAmount: nil,
valueCurrency: nil,
valueUsdAmount: nil,
flags: [],
themePeerId: nil,
peerColor: nil,
hostPeerId: nil,
minOfferStars: nil
)
if let entry = CodableEntry(RecentStarGiftItem(gift)) {
items.append(OrderedItemListEntry(id: RecentStarGiftItemId(id).rawValue, contents: entry))
}
}
default:
break
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedProfilePhotoEmoji(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedProfilePhotoEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.account.getDefaultProfilePhotoEmojis(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .emojiListNotModified:
return .single(nil)
case let .emojiList(_, documentIds):
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: documentIds)
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for fileId in documentIds {
guard let file = files[fileId] else {
continue
}
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry))
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedGroupPhotoEmoji(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedGroupPhotoEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.account.getDefaultGroupPhotoEmojis(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .emojiListNotModified:
return .single(nil)
case let .emojiList(_, documentIds):
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: documentIds)
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for fileId in documentIds {
guard let file = files[fileId] else {
continue
}
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry))
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedBackgroundIconEmoji(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudFeaturedBackgroundIconEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.account.getDefaultBackgroundEmojis(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .emojiListNotModified:
return .single(nil)
case let .emojiList(_, documentIds):
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: documentIds)
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for fileId in documentIds {
guard let file = files[fileId] else {
continue
}
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry))
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedDisabledChannelStatusIconEmoji(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudDisabledChannelStatusEmoji, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.account.getChannelRestrictedStatusEmojis(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .emojiListNotModified:
return .single(nil)
case let .emojiList(_, documentIds):
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: documentIds)
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for fileId in documentIds {
guard let file = files[fileId] else {
continue
}
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry))
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedRecentReactions(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentReactions, extractItemId: { rawId in
switch RecentReactionItemId(rawId).id {
case .builtin:
return 0
case let .custom(fileId):
return fileId.id
case .stars:
return 0
}
}, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.messages.getRecentReactions(limit: 100, hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .reactionsNotModified:
return .single(nil)
case let .reactions(_, reactions):
let parsedReactions = reactions.compactMap(MessageReaction.Reaction.init(apiReaction:))
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedReactions.compactMap { reaction -> Int64? in
switch reaction {
case .builtin:
return nil
case let .custom(fileId):
return fileId
case .stars:
return nil
}
})
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for reaction in parsedReactions {
let item: RecentReactionItem
switch reaction {
case let .builtin(value):
item = RecentReactionItem(.builtin(value))
case let .custom(fileId):
guard let file = files[fileId] else {
continue
}
item = RecentReactionItem(.custom(TelegramMediaFile.Accessor(file)))
case .stars:
item = RecentReactionItem(.stars)
}
if let entry = CodableEntry(item) {
items.append(OrderedItemListEntry(id: item.id.rawValue, contents: entry))
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedTopReactions(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudTopReactions, extractItemId: { rawId in
switch RecentReactionItemId(rawId).id {
case .builtin:
return 0
case let .custom(fileId):
return fileId.id
case .stars:
return 0
}
}, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.messages.getTopReactions(limit: 32, hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .reactionsNotModified:
return .single(nil)
case let .reactions(_, reactions):
let parsedReactions = reactions.compactMap(MessageReaction.Reaction.init(apiReaction:))
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedReactions.compactMap { reaction -> Int64? in
switch reaction {
case .builtin:
return nil
case let .custom(fileId):
return fileId
case .stars:
return nil
}
})
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for reaction in parsedReactions {
let item: RecentReactionItem
switch reaction {
case let .builtin(value):
item = RecentReactionItem(.builtin(value))
case let .custom(fileId):
guard let file = files[fileId] else {
continue
}
item = RecentReactionItem(.custom(TelegramMediaFile.Accessor(file)))
case .stars:
item = RecentReactionItem(.stars)
}
if let entry = CodableEntry(item) {
items.append(OrderedItemListEntry(id: item.id.rawValue, contents: entry))
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedDefaultTagReactions(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudDefaultTagReactions, extractItemId: { rawId in
switch RecentReactionItemId(rawId).id {
case .builtin:
return 0
case let .custom(fileId):
return fileId.id
case .stars:
return 0
}
}, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.messages.getDefaultTagReactions(hash: hash))
|> retryRequestIfNotFrozen
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
guard let result else {
return .single(nil)
}
switch result {
case .reactionsNotModified:
return .single(nil)
case let .reactions(_, reactions):
let parsedReactions = reactions.compactMap(MessageReaction.Reaction.init(apiReaction:))
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: parsedReactions.compactMap { reaction -> Int64? in
switch reaction {
case .builtin:
return nil
case let .custom(fileId):
return fileId
case .stars:
return nil
}
})
|> map { files -> [OrderedItemListEntry] in
var items: [OrderedItemListEntry] = []
for reaction in parsedReactions {
let item: RecentReactionItem
switch reaction {
case let .builtin(value):
item = RecentReactionItem(.builtin(value))
case let .custom(fileId):
guard let file = files[fileId] else {
continue
}
item = RecentReactionItem(.custom(TelegramMediaFile.Accessor(file)))
case .stars:
item = RecentReactionItem(.stars)
}
if let entry = CodableEntry(item) {
items.append(OrderedItemListEntry(id: item.id.rawValue, contents: entry))
}
}
return items
}
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
import Foundation
import Postbox
import SwiftSignalKit
func managedServiceViews(accountPeerId: PeerId, network: Network, postbox: Postbox, stateManager: AccountStateManager, pendingMessageManager: PendingMessageManager) -> (resetPeerHoles: (PeerId) -> Void, disposable: Disposable) {
let disposable = DisposableSet()
let managedHoles = managedMessageHistoryHoles(accountPeerId: accountPeerId, network: network, postbox: postbox)
disposable.add(managedHoles.1)
disposable.add(managedChatListHoles(network: network, postbox: postbox, accountPeerId: accountPeerId).start())
disposable.add(managedForumTopicListHoles(network: network, postbox: postbox, accountPeerId: accountPeerId).start())
disposable.add((_internal_refreshSeenStories(postbox: postbox, network: network) |> then(.complete() |> suspendAwareDelay(5 * 60 * 60, queue: .mainQueue())) |> restart).start())
return (managedHoles.0, disposable)
}
@@ -0,0 +1,134 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizeAppLogEventsOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeAppLogEventsOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizeAppLogEventsOperations(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeAppLogEvents
let helper = Atomic<ManagedSynchronizeAppLogEventsOperationsHelper>(value: ManagedSynchronizeAppLogEventsOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 50).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeAppLogEventsOperation {
return synchronizeAppLogEvents(transaction: transaction, postbox: postbox, network: network, operations: [operation])
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func synchronizeAppLogEvents(transaction: Transaction, postbox: Postbox, network: Network, operations: [SynchronizeAppLogEventsOperation]) -> Signal<Void, NoError> {
var events: [Api.InputAppEvent] = []
for operation in operations {
switch operation.content {
case let .add(time, type, peerId, data):
if let data = apiJson(data) {
events.append(.inputAppEvent(time: time, type: type, peer: peerId?.toInt64() ?? 0, data: data))
}
default:
break
}
}
return network.request(Api.functions.help.saveAppLog(events: events))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
@@ -0,0 +1,253 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizeChatInputStateOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
private let hasRunningOperations: ValuePromise<Bool>
init(hasRunningOperations: ValuePromise<Bool>) {
self.hasRunningOperations = hasRunningOperations
}
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
self.hasRunningOperations.set(!self.operationDisposables.isEmpty)
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeChatInputStateOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizeChatInputStateOperations(postbox: Postbox, network: Network) -> Signal<Bool, NoError> {
return Signal { subscriber in
let hasRunningOperations = ValuePromise<Bool>(false, ignoreRepeated: true)
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeChatInputStates
let helper = Atomic<ManagedSynchronizeChatInputStateOperationsHelper>(value: ManagedSynchronizeChatInputStateOperationsHelper(hasRunningOperations: hasRunningOperations))
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeChatInputStateOperation {
return synchronizeChatInputState(transaction: transaction, postbox: postbox, network: network, peerId: entry.peerId, threadId: operation.threadId, operation: operation)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
let statusDisposable = hasRunningOperations.get().start(next: { value in
subscriber.putNext(value)
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
statusDisposable.dispose()
}
}
}
private func synchronizeChatInputState(transaction: Transaction, postbox: Postbox, network: Network, peerId: PeerId, threadId: Int64?, operation: SynchronizeChatInputStateOperation) -> Signal<Void, NoError> {
var inputState: SynchronizeableChatInputState?
let peerChatInterfaceState: StoredPeerChatInterfaceState?
if let threadId {
peerChatInterfaceState = transaction.getPeerChatThreadInterfaceState(peerId, threadId: threadId)
} else {
peerChatInterfaceState = transaction.getPeerChatInterfaceState(peerId)
}
if let peerChatInterfaceState = peerChatInterfaceState, let data = peerChatInterfaceState.data {
inputState = (try? AdaptedPostboxDecoder().decode(InternalChatInterfaceState.self, from: data))?.synchronizeableInputState
}
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0
if let inputState = inputState {
if !inputState.entities.isEmpty {
flags |= (1 << 3)
}
}
var topMsgId: Int32?
var monoforumPeerId: Api.InputPeer?
if let threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
monoforumPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer)
} else {
topMsgId = Int32(clamping: threadId)
}
}
var replyTo: Api.InputReplyTo?
if let replySubject = inputState?.replySubject {
flags |= 1 << 0
var innerFlags: Int32 = 0
if topMsgId != nil {
innerFlags |= 1 << 0
} else if monoforumPeerId != nil {
innerFlags |= 1 << 5
}
var replyToPeer: Api.InputPeer?
var discard = false
if replySubject.messageId.peerId != peerId {
replyToPeer = transaction.getPeer(replySubject.messageId.peerId).flatMap(apiInputPeer)
if replyToPeer == nil {
discard = true
}
}
var quoteText: String?
var quoteEntities: [Api.MessageEntity]?
var quoteOffset: Int32?
if let replyQuote = replySubject.quote {
quoteText = replyQuote.text
quoteOffset = replyQuote.offset.flatMap { Int32(clamping: $0) }
if !replyQuote.entities.isEmpty {
var associatedPeers = SimpleDictionary<PeerId, Peer>()
for entity in replyQuote.entities {
for associatedPeerId in entity.associatedPeerIds {
if associatedPeers[associatedPeerId] == nil {
if let associatedPeer = transaction.getPeer(associatedPeerId) {
associatedPeers[associatedPeerId] = associatedPeer
}
}
}
}
quoteEntities = apiEntitiesFromMessageTextEntities(replyQuote.entities, associatedPeers: associatedPeers)
}
}
let replyTodoItemId = replySubject.todoItemId
if replyToPeer != nil {
innerFlags |= 1 << 1
}
if quoteText != nil {
innerFlags |= 1 << 2
}
if quoteEntities != nil {
innerFlags |= 1 << 3
}
if quoteOffset != nil {
innerFlags |= 1 << 4
}
if let _ = replyTodoItemId {
innerFlags |= 1 << 6
}
if !discard {
replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: replySubject.messageId.id, topMsgId: topMsgId, replyToPeerId: replyToPeer, quoteText: quoteText, quoteEntities: quoteEntities, quoteOffset: quoteOffset, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId)
}
} else if let topMsgId {
flags |= 1 << 0
var innerFlags: Int32 = 0
innerFlags |= 1 << 0
replyTo = .inputReplyToMessage(flags: innerFlags, replyToMsgId: topMsgId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil)
} else if let monoforumPeerId {
flags |= 1 << 0
replyTo = .inputReplyToMonoForum(monoforumPeerId: monoforumPeerId)
}
let suggestedPost = inputState?.suggestedPost.flatMap { suggestedPost -> Api.SuggestedPost in
var flags: Int32 = 0
if suggestedPost.timestamp != nil {
flags |= 1 << 0
}
return .suggestedPost(flags: flags, price: suggestedPost.price?.apiAmount ?? .starsAmount(amount: 0, nanos: 0), scheduleDate: suggestedPost.timestamp)
}
if suggestedPost != nil {
flags |= 1 << 8
}
return network.request(Api.functions.messages.saveDraft(flags: flags, replyTo: replyTo, peer: inputPeer, message: inputState?.text ?? "", entities: apiEntitiesFromMessageTextEntities(inputState?.entities ?? [], associatedPeers: SimpleDictionary()), media: nil, effect: nil, suggestedPost: suggestedPost))
|> delay(2.0, queue: Queue.concurrentDefaultQueue())
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
} else {
return .complete()
}
}
@@ -0,0 +1,144 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizeConsumeMessageContentsOperationHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SynchronizeConsumeMessageContents, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeConsumeMessageContentsOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizeConsumeMessageContentOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedSynchronizeConsumeMessageContentsOperationHelper>(value: ManagedSynchronizeConsumeMessageContentsOperationHelper())
let disposable = postbox.mergedOperationLogView(tag: OperationLogTags.SynchronizeConsumeMessageContents, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeConsumeMessageContentsOperation {
return synchronizeConsumeMessageContents(transaction: transaction, network: network, stateManager: stateManager, peerId: entry.peerId, operation: operation)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: OperationLogTags.SynchronizeConsumeMessageContents, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func synchronizeConsumeMessageContents(transaction: Transaction, network: Network, stateManager: AccountStateManager, peerId: PeerId, operation: SynchronizeConsumeMessageContentsOperation) -> Signal<Void, NoError> {
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup {
return network.request(Api.functions.messages.readMessageContents(id: operation.messageIds.map { $0.id }))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AffectedMessages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .affectedMessages(pts, ptsCount):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
}
}
return .complete()
}
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) {
return network.request(Api.functions.channels.readMessageContents(channel: inputChannel, id: operation.messageIds.map { $0.id }))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Bool?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
return .complete()
}
} else {
return .complete()
}
} else {
return .complete()
}
}
@@ -0,0 +1,224 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
private final class ManagedSynchronizeEmojiKeywordsOperationHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SynchronizeEmojiKeywords, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeEmojiKeywordsOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizeEmojiKeywordsOperations(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let tag = OperationLogTags.SynchronizeEmojiKeywords
return Signal { _ in
let helper = Atomic<ManagedSynchronizeEmojiKeywordsOperationHelper>(value: ManagedSynchronizeEmojiKeywordsOperationHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeEmojiKeywordsOperation {
let collectionId = emojiKeywordColletionIdForCode(operation.inputLanguageCode)
return synchronizeEmojiKeywords(postbox: postbox, transaction: transaction, network: network, operation: operation, collectionId: collectionId)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func keywordCollectionItemId(_ keyword: String, inputLanguageCode: String) -> Int64 {
let namespace = HashFunctions.murMurHash32(inputLanguageCode)
let id = HashFunctions.murMurHash32(keyword)
return (Int64(namespace) << 32) | Int64(bitPattern: UInt64(UInt32(bitPattern: id)))
}
private func synchronizeEmojiKeywords(postbox: Postbox, transaction: Transaction, network: Network, operation: SynchronizeEmojiKeywordsOperation, collectionId: ItemCollectionId) -> Signal<Void, NoError> {
if let languageCode = operation.languageCode, let fromVersion = operation.fromVersion {
return network.request(Api.functions.messages.getEmojiKeywordsDifference(langCode: languageCode, fromVersion: fromVersion))
|> retryRequest
|> mapToSignal { result -> Signal<Void, NoError> in
switch result {
case let .emojiKeywordsDifference(langCode, _, version, keywords):
if langCode == languageCode {
var itemsToAppend: [String: EmojiKeywordItem] = [:]
var itemsToSubtract: [String: EmojiKeywordItem] = [:]
for apiEmojiKeyword in keywords {
switch apiEmojiKeyword {
case let .emojiKeyword(keyword, emoticons):
let keyword = keyword.replacingOccurrences(of: " ", with: "")
let indexKeys = stringIndexTokens(keyword, transliteration: .none).map { $0.toMemoryBuffer() }
let item = EmojiKeywordItem(index: ItemCollectionItemIndex(index: 0, id: 0), collectionId: collectionId.id, keyword: keyword, emoticons: emoticons, indexKeys: indexKeys)
itemsToAppend[keyword] = item
case let .emojiKeywordDeleted(keyword, emoticons):
let item = EmojiKeywordItem(index: ItemCollectionItemIndex(index: 0, id: 0), collectionId: collectionId.id, keyword: keyword, emoticons: emoticons, indexKeys: [])
itemsToSubtract[keyword] = item
}
}
let info = EmojiKeywordCollectionInfo(languageCode: langCode, inputLanguageCode: operation.inputLanguageCode, version: version, timestamp: Int32(CFAbsoluteTimeGetCurrent()))
return postbox.transaction { transaction -> Void in
var updatedInfos = transaction.getItemCollectionsInfos(namespace: info.id.namespace).map { $0.1 as! EmojiKeywordCollectionInfo }
if let index = updatedInfos.firstIndex(where: { $0.id == info.id }) {
updatedInfos.remove(at: index)
}
updatedInfos.append(info)
if fromVersion != version {
let currentItems = transaction.getItemCollectionItems(collectionId: collectionId)
var updatedItems: [EmojiKeywordItem] = []
var index: Int32 = 0
for case let item as EmojiKeywordItem in currentItems {
var updatedEmoticons = item.emoticons
var existingEmoticons = Set(item.emoticons)
if let appendedItem = itemsToAppend[item.keyword] {
for emoticon in appendedItem.emoticons {
if !existingEmoticons.contains(emoticon) {
existingEmoticons.insert(emoticon)
updatedEmoticons.append(emoticon)
}
}
itemsToAppend.removeValue(forKey: item.keyword)
}
if let subtractedItem = itemsToSubtract[item.keyword] {
let substractedEmoticons = Set(subtractedItem.emoticons)
updatedEmoticons = updatedEmoticons.filter { !substractedEmoticons.contains($0) }
}
if !updatedEmoticons.isEmpty {
updatedItems.append(EmojiKeywordItem(index: ItemCollectionItemIndex(index: index, id: keywordCollectionItemId(item.keyword, inputLanguageCode: operation.inputLanguageCode)), collectionId: item.collectionId, keyword: item.keyword, emoticons: updatedEmoticons, indexKeys: item.indexKeys))
index += 1
}
}
for (_, item) in itemsToAppend where !item.emoticons.isEmpty {
updatedItems.append(EmojiKeywordItem(index: ItemCollectionItemIndex(index: index, id: keywordCollectionItemId(item.keyword, inputLanguageCode: operation.inputLanguageCode)), collectionId: collectionId.id, keyword: item.keyword, emoticons: item.emoticons, indexKeys: item.indexKeys))
index += 1
}
transaction.replaceItemCollectionItems(collectionId: info.id, items: updatedItems)
}
transaction.replaceItemCollectionInfos(namespace: info.id.namespace, itemCollectionInfos: updatedInfos.map { ($0.id, $0) })
}
} else {
return postbox.transaction { transaction in
addSynchronizeEmojiKeywordsOperation(transaction: transaction, inputLanguageCode: operation.inputLanguageCode, languageCode: nil, fromVersion: nil)
}
}
}
}
} else {
return network.request(Api.functions.messages.getEmojiKeywords(langCode: operation.inputLanguageCode))
|> retryRequest
|> mapToSignal { result -> Signal<Void, NoError> in
switch result {
case let .emojiKeywordsDifference(langCode, _, version, keywords):
var items: [EmojiKeywordItem] = []
var index: Int32 = 0
for apiEmojiKeyword in keywords {
if case let .emojiKeyword(fullKeyword, emoticons) = apiEmojiKeyword, !emoticons.isEmpty {
let keyword = fullKeyword
let indexKeys = stringIndexTokens(keyword, transliteration: .none).map { $0.toMemoryBuffer() }
let item = EmojiKeywordItem(index: ItemCollectionItemIndex(index: index, id: keywordCollectionItemId(keyword, inputLanguageCode: operation.inputLanguageCode)), collectionId: collectionId.id, keyword: keyword, emoticons: emoticons, indexKeys: indexKeys)
items.append(item)
}
index += 1
}
let info = EmojiKeywordCollectionInfo(languageCode: langCode, inputLanguageCode: operation.inputLanguageCode, version: version, timestamp: Int32(CFAbsoluteTimeGetCurrent()))
return postbox.transaction { transaction -> Void in
var updatedInfos = transaction.getItemCollectionsInfos(namespace: info.id.namespace).map { $0.1 as! EmojiKeywordCollectionInfo }
if let index = updatedInfos.firstIndex(where: { $0.id == info.id }) {
updatedInfos.remove(at: index)
}
updatedInfos.append(info)
transaction.replaceItemCollectionInfos(namespace: info.id.namespace, itemCollectionInfos: updatedInfos.map { ($0.id, $0) })
transaction.replaceItemCollectionItems(collectionId: info.id, items: items)
}
}
}
}
}
@@ -0,0 +1,108 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
private final class ManagedSynchronizeGroupMessageStatsState {
private var synchronizeDisposables: [PeerGroupAndNamespace: Disposable] = [:]
func clearDisposables() -> [Disposable] {
let disposables = Array(self.synchronizeDisposables.values)
self.synchronizeDisposables.removeAll()
return disposables
}
func update(operations: Set<PeerGroupAndNamespace>) -> (removed: [Disposable], added: [(PeerGroupAndNamespace, MetaDisposable)]) {
var removed: [Disposable] = []
var added: [(PeerGroupAndNamespace, MetaDisposable)] = []
for (groupAndNamespace, disposable) in self.synchronizeDisposables {
if !operations.contains(groupAndNamespace) {
removed.append(disposable)
self.synchronizeDisposables.removeValue(forKey: groupAndNamespace)
}
}
for groupAndNamespace in operations {
if self.synchronizeDisposables[groupAndNamespace] == nil {
let disposable = MetaDisposable()
self.synchronizeDisposables[groupAndNamespace] = disposable
added.append((groupAndNamespace, disposable))
}
}
return (removed, added)
}
}
func managedSynchronizeGroupMessageStats(network: Network, postbox: Postbox, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let state = Atomic(value: ManagedSynchronizeGroupMessageStatsState())
let disposable = postbox.combinedView(keys: [.synchronizeGroupMessageStats]).start(next: { views in
let (removed, added) = state.with { state -> (removed: [Disposable], added: [(PeerGroupAndNamespace, MetaDisposable)]) in
let view = views.views[.synchronizeGroupMessageStats] as? SynchronizeGroupMessageStatsView
return state.update(operations: view?.groupsAndNamespaces ?? Set())
}
for disposable in removed {
disposable.dispose()
}
for (groupAndNamespace, disposable) in added {
let synchronizeOperation = synchronizeGroupMessageStats(postbox: postbox, network: network, groupId: groupAndNamespace.groupId, namespace: groupAndNamespace.namespace)
disposable.set(synchronizeOperation.start())
}
})
return ActionDisposable {
disposable.dispose()
for disposable in state.with({ state -> [Disposable] in
state.clearDisposables()
}) {
disposable.dispose()
}
}
}
}
private func synchronizeGroupMessageStats(postbox: Postbox, network: Network, groupId: PeerGroupId, namespace: MessageId.Namespace) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
if namespace != Namespaces.Message.Cloud || groupId == .root {
transaction.confirmSynchronizedPeerGroupMessageStats(groupId: groupId, namespace: namespace)
return .complete()
}
if !transaction.doesChatListGroupContainHoles(groupId: groupId) {
transaction.recalculateChatListGroupStats(groupId: groupId)
return .complete()
}
return network.request(Api.functions.messages.getPeerDialogs(peers: [.inputDialogPeerFolder(folderId: groupId.rawValue)]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.PeerDialogs?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction in
if let result = result {
switch result {
case let .peerDialogs(dialogs, _, _, _, _):
for dialog in dialogs {
switch dialog {
case let .dialogFolder(_, _, _, _, unreadMutedPeersCount, _, unreadMutedMessagesCount, _):
transaction.resetPeerGroupSummary(groupId: groupId, namespace: namespace, summary: PeerGroupUnreadCountersSummary(all: PeerGroupUnreadCounters(messageCount: unreadMutedMessagesCount, chatCount: unreadMutedPeersCount)))
case .dialog:
assertionFailure()
break
}
}
}
}
transaction.confirmSynchronizedPeerGroupMessageStats(groupId: groupId, namespace: namespace)
}
}
}
|> switchToLatest
}
@@ -0,0 +1,156 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizeGroupedPeersOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var validMergedIndices = Set<Int32>()
for entry in entries {
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperations(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndices: [Int32], _ f: @escaping (Transaction, [PeerMergedOperationLogEntry]) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: [PeerMergedOperationLogEntry] = []
for tagLocalIndex in tagLocalIndices {
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeGroupedPeersOperation {
result.append(entry.mergedEntry!)
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
}
return f(transaction, result)
}
|> switchToLatest
}
func managedSynchronizeGroupedPeersOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeGroupedPeers
let helper = Atomic<ManagedSynchronizeGroupedPeersOperationsHelper>(value: ManagedSynchronizeGroupedPeersOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 100).start(next: { view in
let (disposeOperations, sharedBeginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
var beginOperationsByPeerId: [PeerId: [(PeerMergedOperationLogEntry, MetaDisposable)]] = [:]
for (entry, disposable) in sharedBeginOperations {
if beginOperationsByPeerId[entry.peerId] == nil {
beginOperationsByPeerId[entry.peerId] = []
}
beginOperationsByPeerId[entry.peerId]?.append((entry, disposable))
}
if !beginOperationsByPeerId.isEmpty {
for (peerId, peerOperations) in beginOperationsByPeerId {
let localIndices = Array(peerOperations.map({ $0.0.tagLocalIndex }))
let sharedDisposable = MetaDisposable()
for (_, disposable) in peerOperations {
disposable.set(sharedDisposable)
}
let signal = withTakenOperations(postbox: postbox, peerId: peerId, tag: tag, tagLocalIndices: localIndices, { transaction, entries -> Signal<Void, NoError> in
if !entries.isEmpty {
let operations = entries.compactMap({ $0.contents as? SynchronizeGroupedPeersOperation })
if !operations.isEmpty {
return synchronizeGroupedPeers(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, operations: operations)
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
for tagLocalIndex in localIndices {
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex)
}
})
sharedDisposable.set(signal.start())
}
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func synchronizeGroupedPeers(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, operations: [SynchronizeGroupedPeersOperation]) -> Signal<Void, NoError> {
if operations.isEmpty {
return .complete()
}
var folderPeers: [Api.InputFolderPeer] = []
for operation in operations {
if let inputPeer = transaction.getPeer(operation.peerId).flatMap(apiInputPeer) {
folderPeers.append(.inputFolderPeer(peer: inputPeer, folderId: operation.groupId.rawValue))
}
}
if folderPeers.isEmpty {
return .complete()
}
return network.request(Api.functions.folders.editPeerFolders(folderPeers: folderPeers))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
stateManager.addUpdates(updates)
}
return .complete()
}
}
@@ -0,0 +1,731 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizeInstalledStickerPacksOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeInstalledStickerPacksOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizeInstalledStickerPacksOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag
switch namespace {
case .stickers:
tag = OperationLogTags.SynchronizeInstalledStickerPacks
case .masks:
tag = OperationLogTags.SynchronizeInstalledMasks
case .emoji:
tag = OperationLogTags.SynchronizeInstalledEmoji
}
let helper = Atomic<ManagedSynchronizeInstalledStickerPacksOperationsHelper>(value: ManagedSynchronizeInstalledStickerPacksOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeInstalledStickerPacksOperation {
return stateManager.isUpdating
|> filter { !$0 }
|> take(1)
|> mapToSignal { _ -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Signal<Void, NoError> in
return synchronizeInstalledStickerPacks(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, namespace: namespace, operation: operation)
}
|> switchToLatest
}
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set((signal |> delay(0.0, queue: Queue.concurrentDefaultQueue())).start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func hashForStickerPackInfos(_ infos: [StickerPackCollectionInfo]) -> Int64 {
var acc: UInt64 = 0
for info in infos {
combineInt64Hash(&acc, with: UInt64(bitPattern: Int64(info.hash)))
}
return finalizeInt64Hash(acc)
}
private enum SynchronizeInstalledStickerPacksError {
case restart
case done
case frozen
}
private func fetchStickerPack(network: Network, info: StickerPackCollectionInfo) -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> {
return network.request(Api.functions.messages.getStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), hash: 0))
|> map { result -> (StickerPackCollectionInfo, [ItemCollectionItem]) in
var items: [ItemCollectionItem] = []
var updatedInfo = info
switch result {
case .stickerSetNotModified:
break
case let .stickerSet(stickerSet, packs, keywords, documents):
updatedInfo = StickerPackCollectionInfo(apiSet: stickerSet, namespace: info.id.namespace)
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs {
switch pack {
case let .stickerPack(text, fileIds):
let key = ValueBoxKey(text).toMemoryBuffer()
for fileId in fileIds {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
break
}
}
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for apiDocument in documents {
if let file = telegramMediaFileFromApiDocument(apiDocument, altDocuments: []), let id = file.id {
let fileIndexKeys: [MemoryBuffer]
if let indexKeys = indexKeysByFile[id] {
fileIndexKeys = indexKeys
} else {
fileIndexKeys = []
}
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: fileIndexKeys))
}
}
break
}
return (updatedInfo, items)
}
|> `catch` { _ -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in
return .single((info, []))
}
}
private func resolveStickerPacks(network: Network, remoteInfos: [ItemCollectionId: (StickerPackCollectionInfo, Bool)], localInfos: [ItemCollectionId: StickerPackCollectionInfo]) -> Signal<[ItemCollectionId: [ItemCollectionItem]], NoError> {
var signals: [Signal<(ItemCollectionId, [ItemCollectionItem]), NoError>] = []
for (id, remoteInfoAndIsStale) in remoteInfos {
let (remoteInfo, isStale) = remoteInfoAndIsStale
let localInfo = localInfos[id]
if localInfo == nil || localInfo!.hash != remoteInfo.hash || isStale {
signals.append(fetchStickerPack(network: network, info: remoteInfo)
|> map { (info, items) in
return (info.id, items)
})
}
}
return combineLatest(signals)
|> map { result -> [ItemCollectionId: [ItemCollectionItem]] in
var dict: [ItemCollectionId: [ItemCollectionItem]] = [:]
for (id, items) in result {
dict[id] = items
}
return dict
}
}
private func installRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal<Set<ItemCollectionId>, NoError> {
var signals: [Signal<Set<ItemCollectionId>, NoError>] = []
for info in infos {
let install = network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolFalse))
|> map { result -> Set<ItemCollectionId> in
switch result {
case .stickerSetInstallResultSuccess:
return Set()
case let .stickerSetInstallResultArchive(archivedSets):
var archivedIds = Set<ItemCollectionId>()
for archivedSet in archivedSets {
switch archivedSet {
case let .stickerSetCovered(set, _):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
case let .stickerSetMultiCovered(set, _):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
case let .stickerSetFullCovered(set, _, _, _):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
case let .stickerSetNoCovered(set):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
}
}
return archivedIds
}
}
|> `catch` { _ -> Signal<Set<ItemCollectionId>, NoError> in
return .single(Set())
}
signals.append(install)
}
return combineLatest(signals)
|> map { idsSets -> Set<ItemCollectionId> in
var result = Set<ItemCollectionId>()
for ids in idsSets {
result.formUnion(ids)
}
return result
}
}
private func removeRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal<Void, NoError> {
if infos.count > 0 {
if infos.count > 1 {
return network.request(Api.functions.messages.toggleStickerSets(flags: 1 << 0, stickersets: infos.map { .inputStickerSetID(id: $0.id.id, accessHash: $0.accessHash) }))
|> mapToSignal { _ -> Signal<Void, MTRpcError> in
return .single(Void())
}
|> `catch` { _ -> Signal<Void, NoError> in
return .single(Void())
}
} else if let info = infos.first {
return network.request(Api.functions.messages.uninstallStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash)))
|> mapToSignal { _ -> Signal<Void, MTRpcError> in
return .single(Void())
}
|> `catch` { _ -> Signal<Void, NoError> in
return .single(Void())
}
}
}
return .single(Void())
}
private func archiveRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal<Void, NoError> {
if infos.count > 0 {
if infos.count > 1 {
return network.request(Api.functions.messages.toggleStickerSets(flags: 1 << 1, stickersets: infos.map { .inputStickerSetID(id: $0.id.id, accessHash: $0.accessHash) }))
|> mapToSignal { _ -> Signal<Void, MTRpcError> in
return .single(Void())
}
|> `catch` { _ -> Signal<Void, NoError> in
return .single(Void())
}
} else if let info = infos.first {
return network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolTrue))
|> mapToSignal { _ -> Signal<Void, MTRpcError> in
return .single(Void())
}
|> `catch` { _ -> Signal<Void, NoError> in
return .single(Void())
}
}
}
return .single(Void())
}
private func reorderRemoteStickerPacks(network: Network, namespace: SynchronizeInstalledStickerPacksOperationNamespace, ids: [ItemCollectionId]) -> Signal<Void, NoError> {
var flags: Int32 = 0
switch namespace {
case .stickers:
break
case .masks:
flags |= (1 << 0)
case .emoji:
flags |= (1 << 1)
}
return network.request(Api.functions.messages.reorderStickerSets(flags: flags, order: ids.map { $0.id }))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
private func synchronizeInstalledStickerPacks(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace, operation: SynchronizeInstalledStickerPacksOperation) -> Signal<Void, NoError> {
let collectionNamespace: ItemCollectionId.Namespace
switch namespace {
case .stickers:
collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks
case .masks:
collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks
case .emoji:
collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks
}
let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
let localStateCollectionOrder = localCollectionInfos.map({ $0.id })
let localPreviousStateCollectionOrder = operation.previousPacks
if localStateCollectionOrder == localPreviousStateCollectionOrder {
return continueSynchronizeInstalledStickerPacks(
transaction: transaction,
postbox: postbox,
network: network,
stateManager: stateManager,
namespace: namespace,
operation: operation
)
}
let locallyAddedInfos = localCollectionInfos.filter { !localPreviousStateCollectionOrder.contains($0.id) }
if locallyAddedInfos.isEmpty {
return continueSynchronizeInstalledStickerPacks(
transaction: transaction,
postbox: postbox,
network: network,
stateManager: stateManager,
namespace: namespace,
operation: operation
)
}
var fetchSignals: [Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError>] = []
for info in locallyAddedInfos {
fetchSignals.append(fetchStickerPack(network: network, info: info))
}
let fetchedPacks = combineLatest(fetchSignals)
return fetchedPacks
|> mapToSignal { fetchedPacks -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Signal<Void, NoError> in
var currentInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
for (info, items) in fetchedPacks {
if let index = currentInfos.firstIndex(where: { $0.id == info.id }) {
currentInfos[index] = info
transaction.replaceItemCollectionItems(collectionId: info.id, items: items)
}
}
transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: currentInfos.map { ($0.id, $0) })
return continueSynchronizeInstalledStickerPacks(
transaction: transaction,
postbox: postbox,
network: network,
stateManager: stateManager,
namespace: namespace,
operation: operation
)
}
|> switchToLatest
}
}
/*#if DEBUG
func debugFetchAllStickers(account: Account) -> Signal<Never, NoError> {
let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers]
let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
let stickerItems: Signal<[TelegramMediaFile], NoError> = account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000)
|> map { view -> [TelegramMediaFile] in
var files: [TelegramMediaFile] = []
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
if !item.file.isAnimatedSticker {
continue
}
files.append(item.file)
}
return files
}
|> take(1)
return stickerItems
|> mapToSignal { files -> Signal<Never, NoError> in
var loadFileSignals: [Signal<Never, NoError>] = []
let tempDir = TempBox.shared.tempDirectory()
print("debugFetchAllStickers into \(tempDir.path)")
for file in files {
loadFileSignals.append(Signal { subscriber in
let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: stickerPackFileReference(file).resourceReference(file.resource)).start()
let data = (account.postbox.mediaBox.resourceData(file.resource)
|> filter { $0.complete }
|> take(1)).start(next: { data in
if let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let unpackedData = TGGUnzipData(dataValue, 5 * 1024 * 1024) {
let filePath = tempDir.path + "/\(file.fileId.id).json"
let _ = try? unpackedData.write(to: URL(fileURLWithPath: filePath), options: .atomic)
subscriber.putCompletion()
}
})
return ActionDisposable {
fetch.dispose()
data.dispose()
}
})
}
return combineLatest(loadFileSignals)
|> ignoreValues
}
}
#endif*/
private func continueSynchronizeInstalledStickerPacks(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace, operation: SynchronizeInstalledStickerPacksOperation) -> Signal<Void, NoError> {
let collectionNamespace: ItemCollectionId.Namespace
switch namespace {
case .stickers:
collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks
case .masks:
collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks
case .emoji:
collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks
}
let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
let initialLocalHash = hashForStickerPackInfos(localCollectionInfos)
let request: Signal<Api.messages.AllStickers, MTRpcError>
switch namespace {
case .stickers:
request = network.request(Api.functions.messages.getAllStickers(hash: initialLocalHash))
case .masks:
request = network.request(Api.functions.messages.getMaskStickers(hash: initialLocalHash))
case .emoji:
request = network.request(Api.functions.messages.getEmojiStickers(hash: initialLocalHash))
}
let sequence = request
|> retryRequestIfNotFrozen
|> mapError { _ -> SynchronizeInstalledStickerPacksError in
}
|> mapToSignal { result -> Signal<Void, SynchronizeInstalledStickerPacksError> in
guard let result else {
return .fail(.frozen)
}
return postbox.transaction { transaction -> Signal<Void, SynchronizeInstalledStickerPacksError> in
let checkLocalCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
if checkLocalCollectionInfos != localCollectionInfos {
return .fail(.restart)
}
let localInitialStateCollectionOrder = operation.previousPacks
let localInitialStateCollectionIds = Set(localInitialStateCollectionOrder)
let localCollectionOrder = checkLocalCollectionInfos.map { $0.id }
let localCollectionIds = Set(localCollectionOrder)
var remoteCollectionInfos: [StickerPackCollectionInfo] = []
switch result {
case let .allStickers(_, sets):
for apiSet in sets {
let info = StickerPackCollectionInfo(apiSet: apiSet, namespace: collectionNamespace)
remoteCollectionInfos.append(info)
}
case .allStickersNotModified:
remoteCollectionInfos = checkLocalCollectionInfos
}
let remoteCollectionOrder = remoteCollectionInfos.map { $0.id }
let remoteCollectionIds = Set(remoteCollectionOrder)
var remoteInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:]
for info in remoteCollectionInfos {
remoteInfos[info.id] = info
}
var localInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:]
for info in checkLocalCollectionInfos {
localInfos[info.id] = info
}
if localInitialStateCollectionOrder == localCollectionOrder {
if checkLocalCollectionInfos == remoteCollectionInfos {
return .fail(.done)
} else {
var resolveRemoteInfos: [ItemCollectionId: (StickerPackCollectionInfo, Bool)] = [:]
for (id, info) in remoteInfos {
resolveRemoteInfos[id] = (info, false)
}
return resolveStickerPacks(network: network, remoteInfos: resolveRemoteInfos, localInfos: localInfos)
|> mapError { _ -> SynchronizeInstalledStickerPacksError in
}
|> mapToSignal { replaceItems -> Signal<Void, SynchronizeInstalledStickerPacksError> in
return postbox.transaction { transaction -> Signal<Void, SynchronizeInstalledStickerPacksError> in
let storeSignal: Signal<Void, NoError>
if localCollectionIds.isEmpty {
var incrementalSignal = postbox.transaction { transaction -> Void in
transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: remoteCollectionInfos.map { ($0.id, $0) })
for id in localCollectionIds.subtracting(remoteCollectionIds) {
transaction.replaceItemCollectionItems(collectionId: id, items: [])
}
}
for (id, items) in replaceItems {
let partSignal = postbox.transaction { transaction -> Void in
transaction.replaceItemCollectionItems(collectionId: id, items: items)
}
incrementalSignal = incrementalSignal
|> then(
partSignal
|> delay(0.01, queue: Queue.concurrentDefaultQueue())
)
}
storeSignal = incrementalSignal
} else {
storeSignal = postbox.transaction { transaction -> Void in
transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: remoteCollectionInfos.map { ($0.id, $0) })
for (id, items) in replaceItems {
transaction.replaceItemCollectionItems(collectionId: id, items: items)
}
for id in localCollectionIds.subtracting(remoteCollectionIds) {
transaction.replaceItemCollectionItems(collectionId: id, items: [])
}
}
}
let storePremiumSignal: Signal<Void, SynchronizeInstalledStickerPacksError> = postbox.transaction { transaction -> Void in
var premiumStickers: [OrderedItemListEntry] = []
for (id, _) in remoteInfos {
let items = transaction.getItemCollectionItems(collectionId: id)
for item in items {
if let stickerItem = item as? StickerPackItem, stickerItem.file.isPremiumSticker,
let entry = CodableEntry(RecentMediaItem(stickerItem.file._parse())) {
premiumStickers.append(OrderedItemListEntry(id: RecentMediaItemId(stickerItem.file.fileId).rawValue, contents: entry))
}
}
}
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.PremiumStickers, items: premiumStickers)
} |> castError(SynchronizeInstalledStickerPacksError.self)
return (
storeSignal
|> mapError { _ -> SynchronizeInstalledStickerPacksError in }
)
|> then(storePremiumSignal)
|> then(.fail(.done))
}
|> castError(SynchronizeInstalledStickerPacksError.self)
|> switchToLatest
}
}
} else {
let locallyRemovedCollectionIds = localInitialStateCollectionIds.subtracting(localCollectionIds)
let locallyAddedCollectionIds = localCollectionIds.subtracting(localInitialStateCollectionIds)
let remotelyAddedCollections = remoteCollectionInfos.filter { info in
return !locallyRemovedCollectionIds.contains(info.id) && !localCollectionIds.contains(info.id)
}
let remotelyRemovedCollectionIds = remoteCollectionIds.subtracting(localInitialStateCollectionIds).subtracting(locallyAddedCollectionIds)
var resultingCollectionInfos: [(StickerPackCollectionInfo, Bool)] = []
resultingCollectionInfos.append(contentsOf: remotelyAddedCollections.map { ($0, false) })
var remoteCollectionInfoMap: [ItemCollectionId: StickerPackCollectionInfo] = [:]
for info in remoteCollectionInfos {
remoteCollectionInfoMap[info.id] = info
}
for info in checkLocalCollectionInfos {
if !remotelyRemovedCollectionIds.contains(info.id) {
if let remoteInfo = remoteCollectionInfoMap[info.id] {
resultingCollectionInfos.append((remoteInfo, false))
} else {
resultingCollectionInfos.append((info, true))
}
}
}
let resultingCollectionIds = Set(resultingCollectionInfos.map { $0.0.id })
let removeRemoteCollectionIds = remoteCollectionIds.subtracting(resultingCollectionIds)
let addRemoteCollectionIds = resultingCollectionIds.subtracting(remoteCollectionIds)
var removeRemoteCollectionInfos: [StickerPackCollectionInfo] = []
for id in removeRemoteCollectionIds {
if let info = remoteInfos[id] {
removeRemoteCollectionInfos.append(info)
} else if let info = localInfos[id] {
removeRemoteCollectionInfos.append(info)
}
}
var addRemoteCollectionInfos: [StickerPackCollectionInfo] = []
for id in addRemoteCollectionIds {
if let info = remoteInfos[id] {
addRemoteCollectionInfos.append(info)
} else if let info = localInfos[id] {
addRemoteCollectionInfos.append(info)
}
}
let infosToArchive = removeRemoteCollectionInfos.filter { info in
return operation.archivedPacks.contains(info.id)
}
let infosToRemove = removeRemoteCollectionInfos.filter { info in
return !operation.archivedPacks.contains(info.id)
}
let archivedOrRemovedIds = (combineLatest(archiveRemoteStickerPacks(network: network, infos: infosToArchive), removeRemoteStickerPacks(network: network, infos: infosToRemove))
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
|> then(Signal<Void, NoError>.single(Void())))
|> mapToSignal { _ -> Signal<Set<ItemCollectionId>, NoError> in
return installRemoteStickerPacks(network: network, infos: addRemoteCollectionInfos)
|> mapToSignal { ids -> Signal<Set<ItemCollectionId>, NoError> in
return (reorderRemoteStickerPacks(network: network, namespace: namespace, ids: resultingCollectionInfos.map({ $0.0.id }).filter({ !ids.contains($0) }))
|> then(Signal<Void, NoError>.single(Void())))
|> map { _ -> Set<ItemCollectionId> in
return ids
}
}
}
var resultingInfos: [ItemCollectionId: (StickerPackCollectionInfo, Bool)] = [:]
for info in resultingCollectionInfos {
resultingInfos[info.0.id] = info
}
let resolvedItems = resolveStickerPacks(network: network, remoteInfos: resultingInfos, localInfos: localInfos)
return combineLatest(archivedOrRemovedIds, resolvedItems)
|> mapError { _ -> SynchronizeInstalledStickerPacksError in }
|> mapToSignal { archivedOrRemovedIds, replaceItems -> Signal<Void, SynchronizeInstalledStickerPacksError> in
return (postbox.transaction { transaction -> Signal<Void, SynchronizeInstalledStickerPacksError> in
let finalCheckLocalCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
if finalCheckLocalCollectionInfos != localCollectionInfos {
return .fail(.restart)
}
transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: resultingCollectionInfos.filter({ info in
return !archivedOrRemovedIds.contains(info.0.id)
}).map({ ($0.0.id, $0.0) }))
for (id, items) in replaceItems {
if !archivedOrRemovedIds.contains(id) {
transaction.replaceItemCollectionItems(collectionId: id, items: items)
}
}
for id in localCollectionIds.subtracting(resultingCollectionIds).union(archivedOrRemovedIds) {
transaction.replaceItemCollectionItems(collectionId: id, items: [])
}
var premiumStickers: [OrderedItemListEntry] = []
for id in resultingCollectionIds {
let items = transaction.getItemCollectionItems(collectionId: id)
for item in items {
if let stickerItem = item as? StickerPackItem, stickerItem.file.isPremiumSticker,
let entry = CodableEntry(RecentMediaItem(stickerItem.file._parse())) {
premiumStickers.append(OrderedItemListEntry(id: RecentMediaItemId(stickerItem.file.fileId).rawValue, contents: entry))
}
}
}
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.PremiumStickers, items: premiumStickers)
return .complete()
}
|> mapError { _ -> SynchronizeInstalledStickerPacksError in
})
|> switchToLatest
|> then(.fail(.done))
}
}
}
|> mapError { _ -> SynchronizeInstalledStickerPacksError in
}
|> switchToLatest
}
return ((sequence
|> `catch` { error -> Signal<Void, SynchronizeInstalledStickerPacksError> in
switch error {
case .frozen:
return .fail(.frozen)
case .done:
return .fail(.done)
case .restart:
return .complete()
}
}) |> restart) |> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
}
@@ -0,0 +1,359 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
private final class ManagedSynchronizeMarkAllUnseenPersonalMessagesOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation<T>(postbox: Postbox, peerId: PeerId, operationType: T.Type, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is T {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
}
|> switchToLatest
}
func managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkAllUnseenPersonalMessages
let helper = Atomic<ManagedSynchronizeMarkAllUnseenPersonalMessagesOperationsHelper>(value: ManagedSynchronizeMarkAllUnseenPersonalMessagesOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, operationType: SynchronizeMarkAllUnseenPersonalMessagesOperation.self, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeMarkAllUnseenPersonalMessagesOperation {
return synchronizeMarkAllUnseen(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, peerId: entry.peerId, operation: operation)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private enum GetUnseenIdsError {
case done
case error(MTRpcError)
}
private func synchronizeMarkAllUnseen(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, operation: SynchronizeMarkAllUnseenPersonalMessagesOperation) -> Signal<Void, NoError> {
guard let inputPeer = transaction.getPeer(peerId).flatMap(apiInputPeer) else {
return .complete()
}
let inputChannel = transaction.getPeer(peerId).flatMap(apiInputChannel)
let limit: Int32 = 100
let oneOperation: (Int32) -> Signal<Int32?, MTRpcError> = { maxId in
return network.request(Api.functions.messages.getUnreadMentions(flags: 0, peer: inputPeer, topMsgId: nil, offsetId: maxId, addOffset: maxId == 0 ? 0 : -1, limit: limit, maxId: maxId == 0 ? 0 : (maxId + 1), minId: 1))
|> mapToSignal { result -> Signal<[MessageId], MTRpcError> in
switch result {
case let .messages(messages, _, _, _):
return .single(messages.compactMap({ $0.id() }))
case let .channelMessages(_, _, _, _, messages, _, _, _):
return .single(messages.compactMap({ $0.id() }))
case .messagesNotModified:
return .single([])
case let .messagesSlice(_, _, _, _, _, messages, _, _, _):
return .single(messages.compactMap({ $0.id() }))
}
}
|> mapToSignal { ids -> Signal<Int32?, MTRpcError> in
let filteredIds = ids.filter { $0.id <= operation.maxId }
if filteredIds.isEmpty {
return .single(ids.min()?.id)
}
if peerId.namespace == Namespaces.Peer.CloudChannel {
guard let inputChannel = inputChannel else {
return .single(nil)
}
return network.request(Api.functions.channels.readMessageContents(channel: inputChannel, id: filteredIds.map { $0.id }))
|> map { result -> Int32? in
if ids.count < limit {
return nil
} else {
return ids.min()?.id
}
}
} else {
return network.request(Api.functions.messages.readMessageContents(id: filteredIds.map { $0.id }))
|> map { result -> Int32? in
switch result {
case let .affectedMessages(pts, ptsCount):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
}
if ids.count < limit {
return nil
} else {
return ids.min()?.id
}
}
}
}
}
let currentMaxId = Atomic<Int32>(value: 0)
let loopOperations: Signal<Void, GetUnseenIdsError> = (
(
deferred {
return oneOperation(currentMaxId.with { $0 })
}
|> `catch` { error -> Signal<Int32?, GetUnseenIdsError> in
return .fail(.error(error))
}
)
|> mapToSignal { resultId -> Signal<Void, GetUnseenIdsError> in
if let resultId = resultId {
let previous = currentMaxId.swap(resultId)
if previous == resultId {
return .fail(.done)
} else {
return .complete()
}
} else {
return .fail(.done)
}
}
|> `catch` { error -> Signal<Void, GetUnseenIdsError> in
switch error {
case .done, .error:
return .fail(error)
}
}
|> restart
)
return loopOperations
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
}
func markUnseenPersonalMessage(transaction: Transaction, id: MessageId, addSynchronizeAction: Bool) {
if let message = transaction.getMessage(id) {
var consume = false
inner: for attribute in message.attributes {
if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.consumed, !attribute.pending {
consume = true
break inner
}
}
if consume {
transaction.updateMessage(id, update: { currentMessage in
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ConsumablePersonalMentionMessageAttribute {
attributes[j] = ConsumablePersonalMentionMessageAttribute(consumed: attribute.consumed, pending: true)
break loop
}
}
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
if addSynchronizeAction {
transaction.setPendingMessageAction(type: .consumeUnseenPersonalMessage, id: id, action: ConsumePersonalMessageAction())
}
}
}
}
func managedSynchronizeMarkAllUnseenReactionsOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkAllUnseenReactions
let helper = Atomic<ManagedSynchronizeMarkAllUnseenPersonalMessagesOperationsHelper>(value: ManagedSynchronizeMarkAllUnseenPersonalMessagesOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, operationType: SynchronizeMarkAllUnseenReactionsOperation.self, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeMarkAllUnseenReactionsOperation {
return synchronizeMarkAllUnseenReactions(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, peerId: entry.peerId, operation: operation)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func synchronizeMarkAllUnseenReactions(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, operation: SynchronizeMarkAllUnseenReactionsOperation) -> Signal<Void, NoError> {
guard let peer = transaction.getPeer(peerId) else {
return .complete()
}
guard let inputPeer = apiInputPeer(peer) else {
return .complete()
}
var flags: Int32 = 0
var topMsgId: Int32?
var savedPeerId: Api.InputPeer?
if let threadId = operation.threadId {
if peer.isMonoForum {
if let subPeerId = transaction.getPeer(PeerId(threadId)).flatMap(apiInputPeer) {
flags |= 1 << 1
savedPeerId = subPeerId
}
} else {
flags |= 1 << 0
topMsgId = Int32(clamping: threadId)
}
}
let signal = network.request(Api.functions.messages.readReactions(flags: flags, peer: inputPeer, topMsgId: topMsgId, savedPeerId: savedPeerId))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AffectedHistory?, Bool> in
return .fail(true)
}
|> mapToSignal { result -> Signal<Void, Bool> in
if let result = result {
switch result {
case let .affectedHistory(pts, ptsCount, offset):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
if offset == 0 {
return .fail(true)
} else {
return .complete()
}
}
} else {
return .fail(true)
}
}
return (signal |> restart)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
}
func markUnseenReactionMessage(transaction: Transaction, id: MessageId, addSynchronizeAction: Bool) {
if let message = transaction.getMessage(id) {
var consume = false
inner: for attribute in message.attributes {
if let attribute = attribute as? ReactionsMessageAttribute, !attribute.hasUnseen {
consume = true
break inner
}
}
if consume {
transaction.updateMessage(id, update: { currentMessage in
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReactionsMessageAttribute {
attributes[j] = attribute.withAllSeen()
break loop
}
}
var tags = currentMessage.tags
tags.remove(.unseenReaction)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
if addSynchronizeAction {
transaction.setPendingMessageAction(type: .readReaction, id: id, action: ReadReactionAction())
}
}
}
}
@@ -0,0 +1,122 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeMarkFeaturedStickerPacksAsSeenOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizeMarkFeaturedStickerPacksAsSeenOperations(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkFeaturedStickerPacksAsSeen
let helper = Atomic<ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperationsHelper>(value: ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeMarkFeaturedStickerPacksAsSeenOperation {
return synchronizeMarkFeaturedStickerPacksAsSeen(transaction: transaction, postbox: postbox, network: network, operation: operation)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func synchronizeMarkFeaturedStickerPacksAsSeen(transaction: Transaction, postbox: Postbox, network: Network, operation: SynchronizeMarkFeaturedStickerPacksAsSeenOperation) -> Signal<Void, NoError> {
return network.request(Api.functions.messages.readFeaturedStickers(id: operation.ids.map { $0.id }))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
@@ -0,0 +1,153 @@
import Foundation
import Postbox
import SwiftSignalKit
private final class SynchronizePeerReadStatesContextImpl {
private final class Operation {
let operation: PeerReadStateSynchronizationOperation
let disposable: Disposable
init(
operation: PeerReadStateSynchronizationOperation,
disposable: Disposable
) {
self.operation = operation
self.disposable = disposable
}
deinit {
self.disposable.dispose()
}
}
private let queue: Queue
private let network: Network
private let postbox: Postbox
private let stateManager: AccountStateManager
private var disposable: Disposable?
private var currentState: [PeerId : PeerReadStateSynchronizationOperation] = [:]
private var activeOperations: [PeerId: Operation] = [:]
private var pendingOperations: [PeerId: PeerReadStateSynchronizationOperation] = [:]
init(queue: Queue, network: Network, postbox: Postbox, stateManager: AccountStateManager) {
self.queue = queue
self.network = network
self.postbox = postbox
self.stateManager = stateManager
self.disposable = (postbox.synchronizePeerReadStatesView()
|> deliverOn(self.queue)).start(next: { [weak self] view in
guard let strongSelf = self else {
return
}
strongSelf.currentState = view.operations
strongSelf.update()
})
}
deinit {
self.disposable?.dispose()
}
func dispose() {
}
private func update() {
let peerIds = Set(self.currentState.keys).union(Set(self.pendingOperations.keys))
for peerId in peerIds {
var maybeOperation: PeerReadStateSynchronizationOperation?
if let operation = self.currentState[peerId] {
maybeOperation = operation
Logger.shared.log("SynchronizePeerReadStates", "\(peerId): take new operation \(operation)")
} else if let operation = self.pendingOperations[peerId] {
maybeOperation = operation
self.pendingOperations.removeValue(forKey: peerId)
Logger.shared.log("SynchronizePeerReadStates", "\(peerId): retrieve pending operation \(operation)")
}
if let operation = maybeOperation {
if let current = self.activeOperations[peerId] {
if current.operation != operation {
Logger.shared.log("SynchronizePeerReadStates", "\(peerId): store pending operation \(operation) (active is \(current.operation))")
self.pendingOperations[peerId] = operation
} else {
Logger.shared.log("SynchronizePeerReadStates", "\(peerId): do nothing, no change in \(operation)")
}
} else {
Logger.shared.log("SynchronizePeerReadStates", "\(peerId): begin operation \(operation)")
let operationDisposable = MetaDisposable()
let activeOperation = Operation(
operation: operation,
disposable: operationDisposable
)
self.activeOperations[peerId] = activeOperation
let signal: Signal<Never, PeerReadStateValidationError>
switch operation {
case .Validate:
signal = synchronizePeerReadState(network: self.network, postbox: self.postbox, stateManager: self.stateManager, peerId: peerId, push: false, validate: true)
|> ignoreValues
case let .Push(_, thenSync):
signal = synchronizePeerReadState(network: self.network, postbox: self.postbox, stateManager: stateManager, peerId: peerId, push: true, validate: thenSync)
|> ignoreValues
}
operationDisposable.set((signal
|> deliverOn(self.queue)).start(error: { [weak self, weak activeOperation] _ in
guard let strongSelf = self else {
return
}
if let activeOperation = activeOperation {
if let current = strongSelf.activeOperations[peerId], current === activeOperation {
Logger.shared.log("SynchronizePeerReadStates", "\(peerId): operation retry \(operation)")
strongSelf.activeOperations.removeValue(forKey: peerId)
strongSelf.update()
}
}
}, completed: { [weak self, weak activeOperation] in
guard let strongSelf = self else {
return
}
if let activeOperation = activeOperation {
if let current = strongSelf.activeOperations[peerId], current === activeOperation {
Logger.shared.log("SynchronizePeerReadStates", "\(peerId): operation completed \(operation)")
strongSelf.activeOperations.removeValue(forKey: peerId)
strongSelf.update()
}
}
}))
}
}
}
}
}
private final class SynchronizePeerReadStatesStatesContext {
private let queue: Queue
private let impl: QueueLocalObject<SynchronizePeerReadStatesContextImpl>
init(network: Network, postbox: Postbox, stateManager: AccountStateManager) {
self.queue = Queue()
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return SynchronizePeerReadStatesContextImpl(queue: queue, network: network, postbox: postbox, stateManager: stateManager)
})
}
func dispose() {
self.impl.with { impl in
impl.dispose()
}
}
}
func managedSynchronizePeerReadStates(network: Network, postbox: Postbox, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let context = SynchronizePeerReadStatesStatesContext(network: network, postbox: postbox, stateManager: stateManager)
return ActionDisposable {
context.dispose()
}
}
}
@@ -0,0 +1,121 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizePeerStoriesOperationsHelper {
var operationDisposables: [PeerId: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var validPeerIds: [PeerId] = []
for entry in entries {
guard let _ = entry.contents as? SynchronizePeerStoriesOperation else {
continue
}
validPeerIds.append(entry.peerId)
var replace = true
if let _ = self.operationDisposables[entry.peerId] {
} else {
replace = true
}
if replace {
let disposable = MetaDisposable()
self.operationDisposables[entry.peerId] = disposable
beginOperations.append((entry, disposable))
}
}
var removedPeerIds: [PeerId] = []
for (peerId, info) in self.operationDisposables {
if !validPeerIds.contains(peerId) {
removedPeerIds.append(peerId)
disposeOperations.append(info)
}
}
for peerId in removedPeerIds {
self.operationDisposables.removeValue(forKey: peerId)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SynchronizePeerStories, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizePeerStoriesOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizePeerStoriesOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedSynchronizePeerStoriesOperationsHelper>(value: ManagedSynchronizePeerStoriesOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: OperationLogTags.SynchronizePeerStories, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizePeerStoriesOperation {
if let peer = transaction.getPeer(entry.peerId) {
return pushStoriesAreSeen(postbox: postbox, network: network, stateManager: stateManager, peer: peer, operation: operation)
} else {
return .complete()
}
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: OperationLogTags.SynchronizePeerStories, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func pushStoriesAreSeen(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: SynchronizePeerStoriesOperation) -> Signal<Void, NoError> {
return _internal_pollPeerStories(postbox: postbox, network: network, accountPeerId: stateManager.accountPeerId, peerId: peer.id, peerReference: PeerReference(peer))
|> map { _ -> Void in
}
}
@@ -0,0 +1,349 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizePinnedChatsOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, tag: PeerOperationLogTag, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizePinnedChatsOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizePinnedChatsOperations(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, tag: PeerOperationLogTag) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedSynchronizePinnedChatsOperationsHelper>(value: ManagedSynchronizePinnedChatsOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, tag: tag, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizePinnedChatsOperation {
if tag == OperationLogTags.SynchronizePinnedChats {
return synchronizePinnedChats(transaction: transaction, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, groupId: PeerGroupId(rawValue: Int32(entry.peerId.id._internalGetInt64Value())), operation: operation)
} else if tag == OperationLogTags.SynchronizePinnedSavedChats {
return synchronizePinnedSavedChats(transaction: transaction, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager, operation: operation)
}
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set((signal |> delay(2.0, queue: Queue.concurrentDefaultQueue())).start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, groupId: PeerGroupId, operation: SynchronizePinnedChatsOperation) -> Signal<Void, NoError> {
let initialRemoteItemIds = operation.previousItemIds
let initialRemoteItemIdsWithoutSecretChats = initialRemoteItemIds.filter { item in
switch item {
case let .peer(peerId):
return peerId.namespace != Namespaces.Peer.SecretChat
}
}
let localItemIds = transaction.getPinnedItemIds(groupId: groupId)
let localItemIdsWithoutSecretChats = localItemIds.filter { item in
switch item {
case let .peer(peerId):
return peerId.namespace != Namespaces.Peer.SecretChat
}
}
return network.request(Api.functions.messages.getPinnedDialogs(folderId: groupId.rawValue))
|> retryRequestIfNotFrozen
|> mapToSignal { dialogs -> Signal<Void, NoError> in
guard let dialogs else {
return .complete()
}
return postbox.transaction { transaction -> Signal<Void, NoError> in
var storeMessages: [StoreMessage] = []
var readStates: [PeerId: [MessageId.Namespace: PeerReadState]] = [:]
var channelStates: [PeerId: Int32] = [:]
var notificationSettings: [PeerId: PeerNotificationSettings] = [:]
var ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout] = [:]
var remoteItemIds: [PinnedItemId] = []
let parsedPeers: AccumulatedPeers
switch dialogs {
case let .peerDialogs(dialogs, messages, chats, users, _):
parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
loop: for dialog in dialogs {
let apiPeer: Api.Peer
let apiReadInboxMaxId: Int32
let apiReadOutboxMaxId: Int32
let apiTopMessage: Int32
let apiUnreadCount: Int32
let apiMarkedUnread: Bool
var apiChannelPts: Int32?
let apiTtlPeriod: Int32?
let apiNotificationSettings: Api.PeerNotifySettings
switch dialog {
case let .dialog(flags, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, _, _, peerNotificationSettings, pts, _, _, ttlPeriod):
apiPeer = peer
apiTopMessage = topMessage
apiReadInboxMaxId = readInboxMaxId
apiReadOutboxMaxId = readOutboxMaxId
apiUnreadCount = unreadCount
apiMarkedUnread = (flags & (1 << 3)) != 0
apiNotificationSettings = peerNotificationSettings
apiChannelPts = pts
apiTtlPeriod = ttlPeriod
case .dialogFolder:
//assertionFailure()
continue loop
}
let peerId: PeerId = apiPeer.peerId
remoteItemIds.append(.peer(peerId))
if readStates[peerId] == nil {
readStates[peerId] = [:]
}
readStates[peerId]![Namespaces.Message.Cloud] = .idBased(maxIncomingReadId: apiReadInboxMaxId, maxOutgoingReadId: apiReadOutboxMaxId, maxKnownId: apiTopMessage, count: apiUnreadCount, markedUnread: apiMarkedUnread)
if let apiChannelPts = apiChannelPts {
channelStates[peerId] = apiChannelPts
}
notificationSettings[peerId] = TelegramPeerNotificationSettings(apiSettings: apiNotificationSettings)
ttlPeriods[peerId] = .known(apiTtlPeriod.flatMap(CachedPeerAutoremoveTimeout.Value.init(peerValue:)))
}
for message in messages {
var peerIsForum = false
if let peerId = message.peerId, let peer = parsedPeers.get(peerId), peer.isForumOrMonoForum {
peerIsForum = true
}
if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) {
storeMessages.append(storeMessage)
}
}
}
var resultingItemIds: [PinnedItemId]
if initialRemoteItemIds == localItemIds {
resultingItemIds = remoteItemIds
} else {
let locallyRemovedFromRemoteItemIds = Set(initialRemoteItemIdsWithoutSecretChats).subtracting(Set(localItemIdsWithoutSecretChats))
let remotelyRemovedItemIds = Set(initialRemoteItemIdsWithoutSecretChats).subtracting(Set(remoteItemIds))
resultingItemIds = localItemIds.filter { !remotelyRemovedItemIds.contains($0) }
resultingItemIds.append(contentsOf: remoteItemIds.filter { !locallyRemovedFromRemoteItemIds.contains($0) && !resultingItemIds.contains($0) })
}
return postbox.transaction { transaction -> Signal<Void, NoError> in
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
transaction.setPinnedItemIds(groupId: groupId, itemIds: resultingItemIds)
transaction.updateCurrentPeerNotificationSettings(notificationSettings)
for (peerId, autoremoveValue) in ttlPeriods {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if peerId.namespace == Namespaces.Peer.CloudUser {
let current = (current as? CachedUserData) ?? CachedUserData()
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
let current = (current as? CachedChannelData) ?? CachedChannelData()
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
let current = (current as? CachedGroupData) ?? CachedGroupData()
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else {
return current
}
})
}
var allPeersWithMessages = Set<PeerId>()
for message in storeMessages {
if !allPeersWithMessages.contains(message.id.peerId) {
allPeersWithMessages.insert(message.id.peerId)
}
}
let _ = transaction.addMessages(storeMessages, location: .UpperHistoryBlock)
transaction.resetIncomingReadStates(readStates)
for (peerId, pts) in channelStates {
if let _ = transaction.getPeerChatState(peerId) as? ChannelState {
// skip changing state
} else {
transaction.setPeerChatState(peerId, state: ChannelState(pts: pts, invalidatedPts: nil, synchronizedUntilMessageId: nil))
}
}
if remoteItemIds == resultingItemIds {
return .complete()
} else {
var inputDialogPeers: [Api.InputDialogPeer] = []
for itemId in resultingItemIds {
switch itemId {
case let .peer(peerId):
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
inputDialogPeers.append(Api.InputDialogPeer.inputDialogPeer(peer: inputPeer))
}
}
}
return network.request(Api.functions.messages.reorderPinnedDialogs(flags: 1 << 0, folderId: groupId.rawValue, order: inputDialogPeers))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(Api.Bool.boolFalse)
}
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
}
}
}
}
|> switchToLatest
}
|> switchToLatest
}
}
private func synchronizePinnedSavedChats(transaction: Transaction, postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, operation: SynchronizePinnedChatsOperation) -> Signal<Void, NoError> {
return network.request(Api.functions.messages.getPinnedSavedDialogs())
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.SavedDialogs?, NoError> in
return .single(nil)
}
|> mapToSignal { dialogs -> Signal<Void, NoError> in
guard let dialogs = dialogs else {
return .never()
}
let _ = dialogs
/*return postbox.transaction { transaction -> Signal<Void, NoError> in
var storeMessages: [StoreMessage] = []
var remoteItemIds: [PeerId] = []
let parsedPeers: AccumulatedPeers
switch dialogs {
case .savedDialogs(let dialogs, let messages, let chats, let users), .savedDialogs(_, let dialogs, let messages, let chats, let users):
parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
loop: for dialog in dialogs {
switch dialog {
case let .savedDialog(_, peer, _):
remoteItemIds.append(peer.peerId)
}
}
for message in messages {
var peerIsForum = false
if let peerId = message.peerId, let peer = parsedPeers.get(peerId), peer.isForumOrMonoForum {
peerIsForum = true
}
if let storeMessage = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peerIsForum) {
storeMessages.append(storeMessage)
}
}
case .savedDialogsNotModified:
parsedPeers = AccumulatedPeers(transaction: transaction, chats: [], users: [])
}
let resultingItemIds: [PeerId] = remoteItemIds
return postbox.transaction { transaction -> Signal<Void, NoError> in
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
transaction.setPeerPinnedThreads(peerId: accountPeerId, threadIds: resultingItemIds.map { $0.toInt64() })
let _ = transaction.addMessages(storeMessages, location: .UpperHistoryBlock)
return .complete()
}
|> switchToLatest
}
|> switchToLatest*/
return .complete()
}
}
@@ -0,0 +1,191 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
private final class ManagedSynchronizeRecentlyUsedMediaOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeRecentlyUsedMediaOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizeRecentlyUsedMediaOperations(accountPeerId: PeerId, postbox: Postbox, network: Network, category: RecentlyUsedMediaCategory, revalidationContext: MediaReferenceRevalidationContext) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag
switch category {
case .stickers:
tag = OperationLogTags.SynchronizeRecentlyUsedStickers
}
let helper = Atomic<ManagedSynchronizeRecentlyUsedMediaOperationsHelper>(value: ManagedSynchronizeRecentlyUsedMediaOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeRecentlyUsedMediaOperation {
return synchronizeRecentlyUsedMedia(transaction: transaction, accountPeerId: accountPeerId, postbox: postbox, network: network, revalidationContext: revalidationContext, operation: operation)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private enum SaveRecentlyUsedMediaError {
case generic
case invalidReference
}
private func synchronizeRecentlyUsedMedia(transaction: Transaction, accountPeerId: PeerId, postbox: Postbox, network: Network, revalidationContext: MediaReferenceRevalidationContext, operation: SynchronizeRecentlyUsedMediaOperation) -> Signal<Void, NoError> {
switch operation.content {
case let .add(id, accessHash, fileReference):
guard let fileReference = fileReference else {
return .complete()
}
let addSticker: (Data) -> Signal<Api.Bool, SaveRecentlyUsedMediaError> = { fileReference in
return network.request(Api.functions.messages.saveRecentSticker(flags: 0, id: .inputDocument(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference)), unsave: .boolFalse))
|> mapError { error -> SaveRecentlyUsedMediaError in
if error.errorDescription.hasPrefix("FILEREF_INVALID") || error.errorDescription.hasPrefix("FILE_REFERENCE_") {
return .invalidReference
}
return .generic
}
}
let initialSignal: Signal<Api.Bool, SaveRecentlyUsedMediaError>
if let reference = (fileReference.media.resource as? CloudDocumentMediaResource)?.fileReference {
initialSignal = addSticker(reference)
} else {
initialSignal = .fail(.invalidReference)
}
return initialSignal
|> `catch` { error -> Signal<Api.Bool, SaveRecentlyUsedMediaError> in
switch error {
case .generic:
return .fail(.generic)
case .invalidReference:
return revalidateMediaResourceReference(accountPeerId: accountPeerId, postbox: postbox, network: network, revalidationContext: revalidationContext, info: TelegramCloudMediaResourceFetchInfo(reference: fileReference.resourceReference(fileReference.media.resource), preferBackgroundReferenceRevalidation: false, continueInBackground: false), resource: fileReference.media.resource)
|> mapError { _ -> SaveRecentlyUsedMediaError in
return .generic
}
|> mapToSignal { validatedResource -> Signal<Api.Bool, SaveRecentlyUsedMediaError> in
if let resource = validatedResource.updatedResource as? TelegramCloudMediaResourceWithFileReference, let reference = resource.fileReference {
return addSticker(reference)
} else {
return .fail(.generic)
}
}
}
}
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
case let .remove(id, accessHash):
return network.request(Api.functions.messages.saveRecentSticker(flags: 0, id: .inputDocument(id: id, accessHash: accessHash, fileReference: Buffer()), unsave: .boolTrue))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
case .clear:
return network.request(Api.functions.messages.clearRecentStickers(flags: 0))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
case .sync:
return managedRecentStickers(postbox: postbox, network: network)
}
}
@@ -0,0 +1,179 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizeSavedGifsOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(accountPeerId: PeerId, postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeSavedGifsOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizeSavedGifsOperations(accountPeerId: PeerId, postbox: Postbox, network: Network, revalidationContext: MediaReferenceRevalidationContext) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeSavedGifs
let helper = Atomic<ManagedSynchronizeSavedGifsOperationsHelper>(value: ManagedSynchronizeSavedGifsOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(accountPeerId: accountPeerId, postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeSavedGifsOperation {
return synchronizeSavedGifs(transaction: transaction, accountPeerId: accountPeerId, postbox: postbox, network: network, revalidationContext: revalidationContext, operation: operation)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private enum SaveGifError {
case generic
case invalidReference
}
private func synchronizeSavedGifs(transaction: Transaction, accountPeerId: PeerId, postbox: Postbox, network: Network, revalidationContext: MediaReferenceRevalidationContext, operation: SynchronizeSavedGifsOperation) -> Signal<Void, NoError> {
switch operation.content {
case let .add(id, accessHash, fileReference):
guard let fileReference = fileReference else {
return .complete()
}
let saveGif: (Data) -> Signal<Api.Bool, SaveGifError> = { fileReference in
return network.request(Api.functions.messages.saveGif(id: .inputDocument(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference)), unsave: .boolFalse))
|> mapError { error -> SaveGifError in
if error.errorDescription.hasPrefix("FILEREF_INVALID") || error.errorDescription.hasPrefix("FILE_REFERENCE_") {
return .invalidReference
}
return .generic
}
}
let initialSignal: Signal<Api.Bool, SaveGifError>
if let reference = (fileReference.media.resource as? CloudDocumentMediaResource)?.fileReference {
initialSignal = saveGif(reference)
} else {
initialSignal = .fail(.invalidReference)
}
return initialSignal
|> `catch` { error -> Signal<Api.Bool, SaveGifError> in
switch error {
case .generic:
return .fail(.generic)
case .invalidReference:
return revalidateMediaResourceReference(accountPeerId: accountPeerId, postbox: postbox, network: network, revalidationContext: revalidationContext, info: TelegramCloudMediaResourceFetchInfo(reference: fileReference.resourceReference(fileReference.media.resource), preferBackgroundReferenceRevalidation: false, continueInBackground: false), resource: fileReference.media.resource)
|> mapError { _ -> SaveGifError in
return .generic
}
|> mapToSignal { validatedResource -> Signal<Api.Bool, SaveGifError> in
if let resource = validatedResource.updatedResource as? TelegramCloudMediaResourceWithFileReference, let reference = resource.fileReference {
return saveGif(reference)
} else {
return .fail(.generic)
}
}
}
}
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
case let .remove(id, accessHash):
return network.request(Api.functions.messages.saveGif(id: .inputDocument(id: id, accessHash: accessHash, fileReference: Buffer()), unsave: .boolTrue))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
case .sync:
return managedRecentGifs(postbox: postbox, network: network)
}
}
@@ -0,0 +1,179 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizeSavedStickersOperationsHelper {
var operationDisposables: [Int32: Disposable] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validMergedIndices = Set<Int32>()
for entry in entries {
if !hasRunningOperationForPeerId.contains(entry.peerId) {
hasRunningOperationForPeerId.insert(entry.peerId)
validMergedIndices.insert(entry.mergedIndex)
if self.operationDisposables[entry.mergedIndex] == nil {
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.mergedIndex] = disposable
}
}
}
var removeMergedIndices: [Int32] = []
for (mergedIndex, disposable) in self.operationDisposables {
if !validMergedIndices.contains(mergedIndex) {
removeMergedIndices.append(mergedIndex)
disposeOperations.append(disposable)
}
}
for mergedIndex in removeMergedIndices {
self.operationDisposables.removeValue(forKey: mergedIndex)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values)
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeSavedStickersOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizeSavedStickersOperations(accountPeerId: PeerId, postbox: Postbox, network: Network, revalidationContext: MediaReferenceRevalidationContext) -> Signal<Void, NoError> {
return Signal { _ in
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeSavedStickers
let helper = Atomic<ManagedSynchronizeSavedStickersOperationsHelper>(value: ManagedSynchronizeSavedStickersOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeSavedStickersOperation {
return synchronizeSavedStickers(transaction: transaction, accountPeerId: accountPeerId, postbox: postbox, network: network, revalidationContext: revalidationContext, operation: operation)
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private enum SaveStickerError {
case generic
case invalidReference
}
private func synchronizeSavedStickers(transaction: Transaction, accountPeerId: PeerId, postbox: Postbox, network: Network, revalidationContext: MediaReferenceRevalidationContext, operation: SynchronizeSavedStickersOperation) -> Signal<Void, NoError> {
switch operation.content {
case let .add(id, accessHash, fileReference):
guard let fileReference = fileReference else {
return .complete()
}
let saveSticker: (Data) -> Signal<Api.Bool, SaveStickerError> = { fileReference in
return network.request(Api.functions.messages.faveSticker(id: .inputDocument(id: id, accessHash: accessHash, fileReference: Buffer(data: fileReference)), unfave: .boolFalse))
|> mapError { error -> SaveStickerError in
if error.errorDescription.hasPrefix("FILEREF_INVALID") || error.errorDescription.hasPrefix("FILE_REFERENCE_") {
return .invalidReference
}
return .generic
}
}
let initialSignal: Signal<Api.Bool, SaveStickerError>
if let reference = (fileReference.media.resource as? CloudDocumentMediaResource)?.fileReference {
initialSignal = saveSticker(reference)
} else {
initialSignal = .fail(.invalidReference)
}
return initialSignal
|> `catch` { error -> Signal<Api.Bool, SaveStickerError> in
switch error {
case .generic:
return .fail(.generic)
case .invalidReference:
return revalidateMediaResourceReference(accountPeerId: accountPeerId, postbox: postbox, network: network, revalidationContext: revalidationContext, info: TelegramCloudMediaResourceFetchInfo(reference: fileReference.resourceReference(fileReference.media.resource), preferBackgroundReferenceRevalidation: false, continueInBackground: false), resource: fileReference.media.resource)
|> mapError { _ -> SaveStickerError in
return .generic
}
|> mapToSignal { validatedResource -> Signal<Api.Bool, SaveStickerError> in
if let resource = validatedResource.updatedResource as? TelegramCloudMediaResourceWithFileReference, let reference = resource.fileReference {
return saveSticker(reference)
} else {
return .fail(.generic)
}
}
}
}
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
case let .remove(id, accessHash):
return network.request(Api.functions.messages.faveSticker(id: .inputDocument(id: id, accessHash: accessHash, fileReference: Buffer()), unfave: .boolTrue))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
case .sync:
return managedSavedStickers(postbox: postbox, network: network)
}
}
@@ -0,0 +1,132 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class ManagedSynchronizeViewStoriesOperationsHelper {
var operationDisposables: [PeerId: (Int32, Disposable)] = [:]
func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
var validPeerIds: [PeerId] = []
for entry in entries {
guard let operation = entry.contents as? SynchronizeViewStoriesOperation else {
continue
}
validPeerIds.append(entry.peerId)
var replace = true
if let current = self.operationDisposables[entry.peerId] {
if current.0 != operation.storyId {
disposeOperations.append(current.1)
replace = true
}
} else {
replace = true
}
if replace {
let disposable = MetaDisposable()
self.operationDisposables[entry.peerId] = (operation.storyId, disposable)
beginOperations.append((entry, disposable))
}
}
var removedPeerIds: [PeerId] = []
for (peerId, info) in self.operationDisposables {
if !validPeerIds.contains(peerId) {
removedPeerIds.append(peerId)
disposeOperations.append(info.1)
}
}
for peerId in removedPeerIds {
self.operationDisposables.removeValue(forKey: peerId)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values.map(\.1))
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
var result: PeerMergedOperationLogEntry?
transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SynchronizeViewStories, tagLocalIndex: tagLocalIndex, { entry in
if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeViewStoriesOperation {
result = entry.mergedEntry!
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
} else {
return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
}
})
return f(transaction, result)
} |> switchToLatest
}
func managedSynchronizeViewStoriesOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedSynchronizeViewStoriesOperationsHelper>(value: ManagedSynchronizeViewStoriesOperationsHelper())
let disposable = postbox.mergedOperationLogView(tag: OperationLogTags.SynchronizeViewStories, limit: 10).start(next: { view in
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
return helper.update(view.entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
if let entry = entry {
if let operation = entry.contents as? SynchronizeViewStoriesOperation {
if let peer = transaction.getPeer(entry.peerId) {
return pushStoriesAreSeen(postbox: postbox, network: network, stateManager: stateManager, peer: peer, operation: operation)
} else {
return .complete()
}
} else {
assertionFailure()
}
}
return .complete()
})
|> then(postbox.transaction { transaction -> Void in
let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: OperationLogTags.SynchronizeViewStories, tagLocalIndex: entry.tagLocalIndex)
})
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
private func pushStoriesAreSeen(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: SynchronizeViewStoriesOperation) -> Signal<Void, NoError> {
guard let inputPeer = apiInputPeer(peer) else {
return .complete()
}
return network.request(Api.functions.stories.readStories(peer: inputPeer, maxId: operation.storyId))
|> `catch` { _ -> Signal<[Int32], NoError> in
return .single([])
}
|> map { _ -> Void in
return Void()
}
}
@@ -0,0 +1,27 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func managedVoipConfigurationUpdates(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = Signal<Void, NoError> { subscriber in
return (network.request(Api.functions.phone.getCallConfig())
|> retryRequest
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
switch result {
case let .dataJSON(data):
updateVoipConfiguration(transaction: transaction, { configuration in
var configuration = configuration
configuration.serializedData = data
return configuration
})
}
}
}).start(completed: {
subscriber.putCompletion()
})
}
return (poll |> then(.complete() |> suspendAwareDelay(12.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
@@ -0,0 +1,138 @@
import Foundation
import Postbox
import SwiftSignalKit
private func localIdForResource(_ resource: MediaResource) -> Int64? {
if let resource = resource as? LocalFileMediaResource {
return resource.fileId
}
return nil
}
private final class MessageMediaPreuploadManagerUploadContext {
let disposable = MetaDisposable()
var progress: Float?
var result: MultipartUploadResult?
let subscribers = Bag<(MultipartUploadResult) -> Void>()
deinit {
self.disposable.dispose()
}
}
private final class MessageMediaPreuploadManagerContext {
private let queue: Queue
private var uploadContexts: [Int64: MessageMediaPreuploadManagerUploadContext] = [:]
init(queue: Queue) {
self.queue = queue
assert(self.queue.isCurrent())
}
func add(network: Network, postbox: Postbox, id: Int64, encrypt: Bool, tag: MediaResourceFetchTag?, source: Signal<EngineMediaResource.ResourceData, NoError>, onComplete: (()->Void)? = nil) {
let context = MessageMediaPreuploadManagerUploadContext()
self.uploadContexts[id] = context
let queue = self.queue
context.disposable.set(multipartUpload(network: network, postbox: postbox, source: .custom(source |> map { data in
return MediaResourceData(
path: data.path,
offset: 0,
size: data.availableSize,
complete: data.isComplete
)
}), encrypt: encrypt, tag: tag, hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false).start(next: { [weak self] next in
queue.async {
if let strongSelf = self, let context = strongSelf.uploadContexts[id] {
switch next {
case let .progress(value):
print("progress")
context.progress = value
default:
print("result")
context.result = next
onComplete?()
}
for subscriber in context.subscribers.copyItems() {
subscriber(next)
}
}
}
}))
}
func upload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int64?, hintFileIsLarge: Bool, forceNoBigParts: Bool) -> Signal<MultipartUploadResult, MultipartUploadError> {
let queue = self.queue
return Signal { [weak self] subscriber in
if let strongSelf = self {
if case let .resource(resource) = source, let id = localIdForResource(resource.resource), let context = strongSelf.uploadContexts[id] {
if let result = context.result {
subscriber.putNext(.progress(1.0))
subscriber.putNext(result)
subscriber.putCompletion()
return EmptyDisposable
} else if let progress = context.progress {
subscriber.putNext(.progress(progress))
}
let index = context.subscribers.add({ next in
subscriber.putNext(next)
switch next {
case .inputFile, .inputSecretFile:
subscriber.putCompletion()
case .progress:
break
}
})
return ActionDisposable {
queue.async {
if let strongSelf = self, let context = strongSelf.uploadContexts[id] {
context.subscribers.remove(index)
}
}
}
} else {
return multipartUpload(network: network, postbox: postbox, source: source, encrypt: encrypt, tag: tag, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts).start(next: { next in
subscriber.putNext(next)
}, error: { error in
subscriber.putError(error)
}, completed: {
subscriber.putCompletion()
})
}
} else {
subscriber.putError(.generic)
return EmptyDisposable
}
} |> runOn(self.queue)
}
}
final class MessageMediaPreuploadManager {
private let impl: QueueLocalObject<MessageMediaPreuploadManagerContext>
init() {
let queue = Queue()
self.impl = QueueLocalObject<MessageMediaPreuploadManagerContext>(queue: queue, generate: {
return MessageMediaPreuploadManagerContext(queue: queue)
})
}
func add(network: Network, postbox: Postbox, id: Int64, encrypt: Bool, tag: MediaResourceFetchTag?, source: Signal<EngineMediaResource.ResourceData, NoError>, onComplete:(()->Void)? = nil) {
self.impl.with { context in
context.add(network: network, postbox: postbox, id: id, encrypt: encrypt, tag: tag, source: source, onComplete: onComplete)
}
}
func upload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int64?, hintFileIsLarge: Bool, forceNoBigParts: Bool) -> Signal<MultipartUploadResult, MultipartUploadError> {
return Signal<Signal<MultipartUploadResult, MultipartUploadError>, MultipartUploadError> { subscriber in
self.impl.with { context in
subscriber.putNext(context.upload(network: network, postbox: postbox, source: source, encrypt: encrypt, tag: tag, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts))
subscriber.putCompletion()
}
return EmptyDisposable
}
|> switchToLatest
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,351 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
func _internal_getPaidMessagesRevenue(account: Account, scopePeerId: PeerId, peerId: PeerId) -> Signal<StarsAmount?, NoError> {
return account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputUser?) in
return (transaction.getPeer(scopePeerId).flatMap(apiInputPeer), transaction.getPeer(peerId).flatMap(apiInputUser))
}
|> mapToSignal { scopeInputPeer, inputUser -> Signal<StarsAmount?, NoError> in
if scopePeerId != account.peerId {
if scopeInputPeer == nil {
return .never()
}
}
guard let inputUser else {
return .single(nil)
}
var flags: Int32 = 0
if scopePeerId != account.peerId, scopeInputPeer != nil {
flags |= 1 << 0
}
return account.network.request(Api.functions.account.getPaidMessagesRevenue(flags: flags, parentPeer: scopeInputPeer, userId: inputUser))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.account.PaidMessagesRevenue?, NoError> in
return .single(nil)
}
|> map { result -> StarsAmount? in
guard let result else {
return nil
}
switch result {
case let .paidMessagesRevenue(amount):
return StarsAmount(value: amount, nanos: 0)
}
}
}
}
func _internal_addNoPaidMessagesException(account: Account, scopePeerId: PeerId, peerId: PeerId, refundCharged: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputUser?) in
return (transaction.getPeer(scopePeerId).flatMap(apiInputPeer), transaction.getPeer(peerId).flatMap(apiInputUser))
}
|> mapToSignal { scopeInputPeer, inputUser -> Signal<Never, NoError> in
if scopePeerId != account.peerId {
if scopeInputPeer == nil {
return .never()
}
}
guard let inputUser else {
return .never()
}
var flags: Int32 = 0
if refundCharged {
flags |= (1 << 0)
}
if scopePeerId != account.peerId, scopeInputPeer != nil {
flags |= (1 << 1)
}
return account.network.request(Api.functions.account.toggleNoPaidMessagesException(flags: flags, parentPeer: scopeInputPeer, userId: inputUser))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
} |> mapToSignal { _ in
return account.postbox.transaction { transaction -> Void in
if scopePeerId != account.peerId, scopeInputPeer != nil {
guard var data = transaction.getMessageHistoryThreadInfo(peerId: scopePeerId, threadId: peerId.toInt64())?.data.get(MessageHistoryThreadData.self) else {
return
}
data.isMessageFeeRemoved = true
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: scopePeerId, threadId: peerId.toInt64(), info: entry)
}
} else {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData in
if let cachedData = cachedData as? CachedUserData {
var settings = cachedData.peerStatusSettings ?? .init()
settings.paidMessageStars = nil
return cachedData.withUpdatedPeerStatusSettings(settings)
}
return cachedData
})
}
}
|> ignoreValues
}
|> ignoreValues
}
}
func _internal_reinstateNoPaidMessagesException(account: Account, scopePeerId: PeerId, peerId: PeerId) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> (Api.InputPeer?, Api.InputUser?) in
return (transaction.getPeer(scopePeerId).flatMap(apiInputPeer), transaction.getPeer(peerId).flatMap(apiInputUser))
}
|> mapToSignal { scopeInputPeer, inputUser -> Signal<Never, NoError> in
if scopePeerId != account.peerId {
if scopeInputPeer == nil {
return .never()
}
} else {
return .never()
}
guard let inputUser else {
return .never()
}
var flags: Int32 = 0
flags |= (1 << 2)
flags |= (1 << 1)
return account.network.request(Api.functions.account.toggleNoPaidMessagesException(flags: flags, parentPeer: scopeInputPeer, userId: inputUser))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
} |> mapToSignal { _ in
return account.postbox.transaction { transaction -> Void in
if scopePeerId != account.peerId {
guard var data = transaction.getMessageHistoryThreadInfo(peerId: scopePeerId, threadId: peerId.toInt64())?.data.get(MessageHistoryThreadData.self) else {
return
}
data.isMessageFeeRemoved = false
if let entry = StoredMessageHistoryThreadInfo(data) {
transaction.setMessageHistoryThreadInfo(peerId: scopePeerId, threadId: peerId.toInt64(), info: entry)
}
}
}
|> ignoreValues
}
|> ignoreValues
}
}
func _internal_updateChannelPaidMessagesStars(account: Account, peerId: PeerId, stars: StarsAmount?, broadcastMessagesAllowed: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
guard let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) else {
return .complete()
}
var flags: Int32 = 0
var stars = stars
if broadcastMessagesAllowed {
flags |= (1 << 0)
if stars == nil {
stars = StarsAmount(value: 0, nanos: 0)
}
}
if let channel = peer as? TelegramChannel, case let .broadcast(broadcastInfo) = channel.info {
var infoFlags = broadcastInfo.flags
if broadcastMessagesAllowed {
infoFlags.insert(.hasMonoforum)
} else {
infoFlags.remove(.hasMonoforum)
}
let channel = channel
.withUpdatedInfo(.broadcast(TelegramChannelBroadcastInfo(flags: infoFlags)))
transaction.updatePeersInternal([channel], update: { _, channel in
return channel
})
if let linkedMonoforumId = channel.linkedMonoforumId, let monoforumChannel = transaction.getPeer(linkedMonoforumId) as? TelegramChannel {
let monoforumChannel = monoforumChannel
.withUpdatedSendPaidMessageStars(stars)
transaction.updatePeersInternal([monoforumChannel], update: { _, channel in
return monoforumChannel
})
}
}
return account.network.request(Api.functions.channels.updatePaidMessagesPrice(flags: flags, channel: inputChannel, sendPaidMessagesStars: stars?.value ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
guard let result = result else {
return .complete()
}
account.stateManager.addUpdates(result)
return .complete()
}
}
|> switchToLatest
}
public final class PostponeSendPaidMessageAction: PendingMessageActionData {
public let randomId: Int64
public init(randomId: Int64) {
self.randomId = randomId
}
public init(decoder: PostboxDecoder) {
self.randomId = decoder.decodeInt64ForKey("id", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64(self.randomId, forKey: "id")
}
public func isEqual(to: PendingMessageActionData) -> Bool {
if let other = to as? PostponeSendPaidMessageAction {
if self.randomId != other.randomId {
return false
}
return true
} else {
return false
}
}
}
private final class ManagedApplyPendingPaidMessageActionsHelper {
var operationDisposables: [MessageId: (PendingMessageActionData, Disposable)] = [:]
func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) {
var disposeOperations: [Disposable] = []
var beginOperations: [(PendingMessageActionsEntry, MetaDisposable)] = []
var hasRunningOperationForPeerId = Set<PeerId>()
var validIds = Set<MessageId>()
for entry in entries {
if let current = self.operationDisposables[entry.id], !current.0.isEqual(to: entry.action) {
self.operationDisposables.removeValue(forKey: entry.id)
disposeOperations.append(current.1)
}
if !hasRunningOperationForPeerId.contains(entry.id.peerId) {
hasRunningOperationForPeerId.insert(entry.id.peerId)
validIds.insert(entry.id)
let disposable = MetaDisposable()
beginOperations.append((entry, disposable))
self.operationDisposables[entry.id] = (entry.action, disposable)
}
}
var removeMergedIds: [MessageId] = []
for (id, actionAndDisposable) in self.operationDisposables {
if !validIds.contains(id) {
removeMergedIds.append(id)
disposeOperations.append(actionAndDisposable.1)
}
}
for id in removeMergedIds {
self.operationDisposables.removeValue(forKey: id)
}
return (disposeOperations, beginOperations)
}
func reset() -> [Disposable] {
let disposables = Array(self.operationDisposables.values.map(\.1))
self.operationDisposables.removeAll()
return disposables
}
}
private func withTakenStarsAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal<Never, NoError>) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Signal<Never, NoError> in
var result: PendingMessageActionsEntry?
if let action = transaction.getPendingMessageAction(type: type, id: id) as? PostponeSendPaidMessageAction {
result = PendingMessageActionsEntry(id: id, action: action)
}
return f(transaction, result)
}
|> switchToLatest
}
private func sendPostponedPaidMessage(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal<Never, NoError> {
stateManager.commitSendPendingPaidMessage(messageId: id)
return postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: id, action: nil)
}
|> ignoreValues
}
func managedApplyPendingPaidMessageActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedApplyPendingPaidMessageActionsHelper>(value: ManagedApplyPendingPaidMessageActionsHelper())
let actionsKey = PostboxViewKey.pendingMessageActions(type: .sendPostponedPaidMessage)
let disposable = postbox.combinedView(keys: [actionsKey]).start(next: { view in
var entries: [PendingMessageActionsEntry] = []
if let v = view.views[actionsKey] as? PendingMessageActionsView {
entries = v.entries
}
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) in
return helper.update(entries: entries)
}
for disposable in disposeOperations {
disposable.dispose()
}
for (entry, disposable) in beginOperations {
let signal = withTakenStarsAction(postbox: postbox, type: .sendPostponedPaidMessage, id: entry.id, { transaction, entry -> Signal<Never, NoError> in
if let entry = entry {
if let _ = entry.action as? PostponeSendPaidMessageAction {
let triggerSignal: Signal<Void, NoError> = stateManager.forceSendPendingPaidMessage
|> filter {
$0 == entry.id.peerId
}
|> map { _ -> Void in
return Void()
}
|> take(1)
|> timeout(5.0, queue: .mainQueue(), alternate: .single(Void()))
return triggerSignal
|> mapToSignal { _ -> Signal<Never, NoError> in
return sendPostponedPaidMessage(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, id: entry.id)
}
} else {
assertionFailure()
}
}
return .complete()
})
|> then(
postbox.transaction { transaction -> Void in
transaction.setPendingMessageAction(type: .sendPostponedPaidMessage, id: entry.id, action: nil)
}
|> ignoreValues
)
disposable.set(signal.start())
}
})
return ActionDisposable {
let disposables = helper.with { helper -> [Disposable] in
return helper.reset()
}
for disposable in disposables {
disposable.dispose()
}
disposable.dispose()
}
}
}
func _internal_forceSendPostponedPaidMessage(account: Account, peerId: PeerId) -> Signal<Never, NoError> {
account.stateManager.forceSendPendingPaidMessage(peerId: peerId)
return .complete()
}
@@ -0,0 +1,151 @@
import Foundation
import Postbox
import TelegramApi
public struct EmojiInteraction: Equatable {
public struct Animation: Equatable {
public let index: Int
public let timeOffset: Float
public init(index: Int, timeOffset: Float) {
self.index = index
self.timeOffset = timeOffset
}
}
public let animations: [Animation]
public init(animations: [Animation]) {
self.animations = animations
}
public init?(apiDataJson: Api.DataJSON) {
if case let .dataJSON(string) = apiDataJson, let data = string.data(using: .utf8) {
do {
let decodedData = try JSONSerialization.jsonObject(with: data, options: [])
guard let item = decodedData as? [String: Any] else {
return nil
}
guard let version = item["v"] as? Int, version == 1 else {
return nil
}
guard let animationsArray = item["a"] as? [Any] else {
return nil
}
var animations: [EmojiInteraction.Animation] = []
for animationDict in animationsArray {
if let animationDict = animationDict as? [String: Any] {
if let index = animationDict["i"] as? Int, let timeOffset = animationDict["t"] as? Double {
animations.append(EmojiInteraction.Animation(index: index, timeOffset: Float(timeOffset)))
}
}
}
self.animations = animations
} catch {
return nil
}
} else {
return nil
}
}
fileprivate let roundingBehavior = NSDecimalNumberHandler(roundingMode: .plain, scale: 2, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: true)
public var apiDataJson: Api.DataJSON {
let dict = ["v": 1, "a": self.animations.map({ ["i": $0.index, "t": NSDecimalNumber(value: $0.timeOffset).rounding(accordingToBehavior: roundingBehavior)] as [String : Any] })] as [String : Any]
if let data = try? JSONSerialization.data(withJSONObject: dict, options: []), let dataString = String(data: data, encoding: .utf8) {
return .dataJSON(data: dataString)
} else {
return .dataJSON(data: "")
}
}
}
public enum PeerInputActivity: Comparable {
case typingText
case uploadingFile(progress: Int32)
case recordingVoice
case uploadingPhoto(progress: Int32)
case uploadingVideo(progress: Int32)
case playingGame
case recordingInstantVideo
case uploadingInstantVideo(progress: Int32)
case speakingInGroupCall(timestamp: Int32)
case choosingSticker
case interactingWithEmoji(emoticon: String, messageId: MessageId, interaction: EmojiInteraction?)
case seeingEmojiInteraction(emoticon: String)
public var key: Int32 {
switch self {
case .typingText:
return 0
case .speakingInGroupCall:
return 1
case .uploadingFile:
return 2
case .recordingVoice:
return 3
case .uploadingPhoto:
return 4
case .uploadingVideo:
return 5
case .recordingInstantVideo:
return 6
case .uploadingInstantVideo:
return 7
case .playingGame:
return 8
case .choosingSticker:
return 9
case .interactingWithEmoji:
return 10
case .seeingEmojiInteraction:
return 11
}
}
public static func <(lhs: PeerInputActivity, rhs: PeerInputActivity) -> Bool {
return lhs.key < rhs.key
}
}
extension PeerInputActivity {
init?(apiType: Api.SendMessageAction, peerId: PeerId?, timestamp: Int32) {
switch apiType {
case .sendMessageCancelAction, .sendMessageChooseContactAction, .sendMessageGeoLocationAction, .sendMessageRecordVideoAction:
return nil
case .sendMessageGamePlayAction:
self = .playingGame
case .sendMessageRecordAudioAction, .sendMessageUploadAudioAction:
self = .recordingVoice
case .sendMessageTypingAction:
self = .typingText
case let .sendMessageUploadDocumentAction(progress):
self = .uploadingFile(progress: progress)
case let .sendMessageUploadPhotoAction(progress):
self = .uploadingPhoto(progress: progress)
case let .sendMessageUploadVideoAction(progress):
self = .uploadingVideo(progress: progress)
case .sendMessageRecordRoundAction:
self = .recordingInstantVideo
case let .sendMessageUploadRoundAction(progress):
self = .uploadingInstantVideo(progress: progress)
case .speakingInGroupCallAction:
self = .speakingInGroupCall(timestamp: timestamp)
case .sendMessageChooseStickerAction:
self = .choosingSticker
case .sendMessageHistoryImportAction:
return nil
case let .sendMessageEmojiInteraction(emoticon, messageId, interaction):
if let peerId = peerId {
self = .interactingWithEmoji(emoticon: emoticon, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId), interaction: EmojiInteraction(apiDataJson: interaction))
} else {
return nil
}
case let .sendMessageEmojiInteractionSeen(emoticon):
self = .seeingEmojiInteraction(emoticon: emoticon)
case .sendMessageTextDraftAction:
return nil
}
}
}
@@ -0,0 +1,463 @@
import Foundation
import Postbox
import SwiftSignalKit
private typealias SignalKitTimer = SwiftSignalKit.Timer
private struct ActivityRecord {
let peerId: PeerId
let activity: PeerInputActivity
let id: Int32
let timer: SignalKitTimer
let episodeId: Int32?
let timestamp: Double
let updateId: Int32
}
private final class PeerInputActivityContext {
private let queue: Queue
private let notifyEmpty: () -> Void
private let notifyUpdated: () -> Void
private var nextId: Int32 = 0
private var activities: [ActivityRecord] = []
private let subscribers = Bag<([(PeerId, PeerInputActivityRecord)]) -> Void>()
private var scheduledUpdateSubscribers = false
init(queue: Queue, notifyEmpty: @escaping () -> Void, notifyUpdated: @escaping () -> Void) {
self.queue = queue
self.notifyEmpty = notifyEmpty
self.notifyUpdated = notifyUpdated
}
func addActivity(peerId: PeerId, activity: PeerInputActivity, timeout: Double, episodeId: Int32?, nextUpdateId: inout Int32) {
assert(self.queue.isCurrent())
let timestamp = CFAbsoluteTimeGetCurrent()
var updated = false
var found = false
for i in 0 ..< self.activities.count {
let record = self.activities[i]
if record.peerId == peerId && record.activity.key == activity.key && record.episodeId == episodeId {
found = true
record.timer.invalidate()
var updateId = record.updateId
var recordTimestamp = record.timestamp
if record.activity != activity || record.timestamp + 1.0 < timestamp {
updated = true
updateId = nextUpdateId
recordTimestamp = timestamp
nextUpdateId += 1
}
let currentId = record.id
let timer = SignalKitTimer(timeout: timeout, repeat: false, completion: { [weak self] in
if let strongSelf = self {
for currentActivity in strongSelf.activities {
if currentActivity.id == currentId {
strongSelf.removeActivity(peerId: currentActivity.peerId, activity: currentActivity.activity, episodeId: currentActivity.episodeId)
}
}
}
}, queue: self.queue)
self.activities[i] = ActivityRecord(peerId: peerId, activity: activity, id: currentId, timer: timer, episodeId: episodeId, timestamp: recordTimestamp, updateId: updateId)
timer.start()
break
}
}
if !found {
updated = true
let activityId = self.nextId
self.nextId += 1
let timer = SignalKitTimer(timeout: timeout, repeat: false, completion: { [weak self] in
if let strongSelf = self {
for currentActivity in strongSelf.activities {
if currentActivity.id == activityId {
strongSelf.removeActivity(peerId: currentActivity.peerId, activity: currentActivity.activity, episodeId: currentActivity.episodeId)
}
}
}
}, queue: self.queue)
let updateId = nextUpdateId
nextUpdateId += 1
self.activities.append(ActivityRecord(peerId: peerId, activity: activity, id: activityId, timer: timer, episodeId: episodeId, timestamp: timestamp, updateId: updateId))
timer.start()
}
if updated {
self.scheduleUpdateSubscribers()
}
}
func removeActivity(peerId: PeerId, activity: PeerInputActivity, episodeId: Int32?) {
assert(self.queue.isCurrent())
for i in 0 ..< self.activities.count {
let record = self.activities[i]
if record.peerId == peerId && record.activity.key == activity.key && record.episodeId == episodeId {
self.activities.remove(at: i)
record.timer.invalidate()
self.scheduleUpdateSubscribers()
break
}
}
}
func removeAllActivities(peerId: PeerId) {
assert(self.queue.isCurrent())
var updated = false
for i in (0 ..< self.activities.count).reversed() {
let record = self.activities[i]
if record.peerId == peerId {
record.timer.invalidate()
self.activities.remove(at: i)
updated = true
}
}
if updated {
self.scheduleUpdateSubscribers()
}
}
func scheduleUpdateSubscribers() {
if !self.scheduledUpdateSubscribers {
self.scheduledUpdateSubscribers = true
self.queue.async { [weak self] in
self?.updateSubscribers()
}
}
}
func isEmpty() -> Bool {
return self.activities.isEmpty && self.subscribers.isEmpty
}
func topActivities() -> [(PeerId, PeerInputActivityRecord)] {
var peerIds = Set<PeerId>()
var result: [(PeerId, PeerInputActivityRecord)] = []
for record in self.activities {
if !peerIds.contains(record.peerId) {
peerIds.insert(record.peerId)
result.append((record.peerId, PeerInputActivityRecord(activity: record.activity, updateId: record.updateId)))
if result.count == 10 {
break
}
}
}
return result
}
func updateSubscribers() {
self.scheduledUpdateSubscribers = false
if self.isEmpty() {
self.notifyEmpty()
} else {
let topActivities = self.topActivities()
for subscriber in self.subscribers.copyItems() {
subscriber(topActivities)
}
self.notifyUpdated()
}
}
func addSubscriber(_ subscriber: @escaping ([(PeerId, PeerInputActivityRecord)]) -> Void) -> Int {
return self.subscribers.add(subscriber)
}
func removeSubscriber(_ index: Int) {
self.subscribers.remove(index)
}
}
private final class PeerGlobalInputActivityContext {
private let subscribers = Bag<([PeerActivitySpace: [(PeerId, PeerInputActivityRecord)]]) -> Void>()
func addSubscriber(_ subscriber: @escaping ([PeerActivitySpace: [(PeerId, PeerInputActivityRecord)]]) -> Void) -> Int {
return self.subscribers.add(subscriber)
}
func removeSubscriber(_ index: Int) {
self.subscribers.remove(index)
}
var isEmpty: Bool {
return self.subscribers.isEmpty
}
func notify(_ activities: [PeerActivitySpace: [(PeerId, PeerInputActivityRecord)]]) {
for subscriber in self.subscribers.copyItems() {
subscriber(activities)
}
}
}
final class PeerInputActivityManager {
private final class Impl {
let queue: Queue
var nextEpisodeId: Int32 = 0
var nextUpdateId: Int32 = 0
var contexts: [PeerActivitySpace: PeerInputActivityContext] = [:]
var globalContext: PeerGlobalInputActivityContext?
init(queue: Queue) {
self.queue = queue
}
func activities(peerId: PeerActivitySpace, onNext: @escaping ([(PeerId, PeerInputActivityRecord)]) -> Void) -> Disposable {
let context: PeerInputActivityContext
if let currentContext = self.contexts[peerId] {
context = currentContext
} else {
context = PeerInputActivityContext(queue: queue, notifyEmpty: { [weak self] in
guard let self else {
return
}
self.contexts.removeValue(forKey: peerId)
if let globalContext = self.globalContext {
let activities = self.collectActivities()
globalContext.notify(activities)
}
}, notifyUpdated: { [weak self] in
guard let self else {
return
}
if let globalContext = self.globalContext {
let activities = self.collectActivities()
globalContext.notify(activities)
}
})
self.contexts[peerId] = context
}
let index = context.addSubscriber({ next in
onNext(next)
})
onNext(context.topActivities())
let queue = self.queue
return ActionDisposable { [weak self] in
queue.async {
guard let self else {
return
}
if let currentContext = self.contexts[peerId] {
currentContext.removeSubscriber(index)
if currentContext.isEmpty() {
self.contexts.removeValue(forKey: peerId)
}
}
}
}
}
func allActivities(onNext: @escaping ([PeerActivitySpace: [(PeerId, PeerInputActivityRecord)]]) -> Void) -> Disposable {
let context: PeerGlobalInputActivityContext
if let current = self.globalContext {
context = current
} else {
context = PeerGlobalInputActivityContext()
self.globalContext = context
}
let index = context.addSubscriber({ next in
onNext(next)
})
onNext(self.collectActivities())
let queue = self.queue
return ActionDisposable { [weak self] in
queue.async {
guard let self else {
return
}
if let currentContext = self.globalContext {
currentContext.removeSubscriber(index)
if currentContext.isEmpty {
self.globalContext = nil
}
}
}
}
}
private func collectActivities() -> [PeerActivitySpace: [(PeerId, PeerInputActivityRecord)]] {
assert(self.queue.isCurrent())
var dict: [PeerActivitySpace: [(PeerId, PeerInputActivityRecord)]] = [:]
for (chatPeerId, context) in self.contexts {
dict[chatPeerId] = context.topActivities()
}
return dict
}
func addActivity(chatPeerId: PeerActivitySpace, peerId: PeerId, activity: PeerInputActivity, episodeId: Int32?) {
let context: PeerInputActivityContext
if let currentContext = self.contexts[chatPeerId] {
context = currentContext
} else {
context = PeerInputActivityContext(queue: self.queue, notifyEmpty: { [weak self] in
guard let self else {
return
}
self.contexts.removeValue(forKey: chatPeerId)
if let globalContext = self.globalContext {
let activities = self.collectActivities()
globalContext.notify(activities)
}
}, notifyUpdated: { [weak self] in
guard let self else {
return
}
if let globalContext = self.globalContext {
let activities = self.collectActivities()
globalContext.notify(activities)
}
})
self.contexts[chatPeerId] = context
}
let timeout: Double
switch activity {
case .interactingWithEmoji:
timeout = 2.0
case .speakingInGroupCall, .seeingEmojiInteraction:
timeout = 3.0
default:
timeout = 8.0
}
if activity == .choosingSticker {
context.removeActivity(peerId: peerId, activity: .typingText, episodeId: nil)
}
context.addActivity(peerId: peerId, activity: activity, timeout: timeout, episodeId: episodeId, nextUpdateId: &self.nextUpdateId)
if let globalContext = self.globalContext {
let activities = self.collectActivities()
globalContext.notify(activities)
}
}
func removeActivity(chatPeerId: PeerActivitySpace, peerId: PeerId, activity: PeerInputActivity, episodeId: Int32?) {
if let context = self.contexts[chatPeerId] {
context.removeActivity(peerId: peerId, activity: activity, episodeId: episodeId)
if let globalContext = self.globalContext {
let activities = self.collectActivities()
globalContext.notify(activities)
}
}
}
func removeAllActivities(chatPeerId: PeerActivitySpace, peerId: PeerId) {
if let currentContext = self.contexts[chatPeerId] {
currentContext.removeAllActivities(peerId: peerId)
if let globalContext = self.globalContext {
let activities = self.collectActivities()
globalContext.notify(activities)
}
}
}
func acquireActivity(chatPeerId: PeerActivitySpace, peerId: PeerId, activity: PeerInputActivity) -> Disposable {
let queue = self.queue
let episodeId = self.nextEpisodeId
self.nextEpisodeId += 1
let update: () -> Void = { [weak self] in
guard let self else {
return
}
self.addActivity(chatPeerId: chatPeerId, peerId: peerId, activity: activity, episodeId: episodeId)
}
let timeout: Double
switch activity {
case .speakingInGroupCall:
timeout = 2.0
default:
timeout = 5.0
}
let timer = SignalKitTimer(timeout: timeout, repeat: true, completion: {
update()
}, queue: queue)
timer.start()
update()
return ActionDisposable { [weak self] in
queue.async {
timer.invalidate()
guard let self else {
return
}
self.removeActivity(chatPeerId: chatPeerId, peerId: peerId, activity: activity, episodeId: episodeId)
}
}
}
}
private let queue = Queue()
private let impl: QueueLocalObject<Impl>
init() {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue)
})
}
func activities(peerId: PeerActivitySpace) -> Signal<[(PeerId, PeerInputActivityRecord)], NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.activities(peerId: peerId, onNext: subscriber.putNext)
}
}
func allActivities() -> Signal<[PeerActivitySpace: [(PeerId, PeerInputActivityRecord)]], NoError> {
return self.impl.signalWith { impl, subscriber in
return impl.allActivities(onNext: subscriber.putNext)
}
}
func addActivity(chatPeerId: PeerActivitySpace, peerId: PeerId, activity: PeerInputActivity, episodeId: Int32? = nil) {
self.impl.with { impl in
impl.addActivity(chatPeerId: chatPeerId, peerId: peerId, activity: activity, episodeId: episodeId)
}
}
func removeActivity(chatPeerId: PeerActivitySpace, peerId: PeerId, activity: PeerInputActivity, episodeId: Int32? = nil) {
self.impl.with { impl in
impl.removeActivity(chatPeerId: chatPeerId, peerId: peerId, activity: activity, episodeId: episodeId)
}
}
func removeAllActivities(chatPeerId: PeerActivitySpace, peerId: PeerId) {
self.impl.with { impl in
impl.removeAllActivities(chatPeerId: chatPeerId, peerId: peerId)
}
}
func transaction(_ f: @escaping (PeerInputActivityManager) -> Void) {
self.queue.async {
f(self)
}
}
func acquireActivity(chatPeerId: PeerActivitySpace, peerId: PeerId, activity: PeerInputActivity) -> Disposable {
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.acquireActivity(chatPeerId: chatPeerId, peerId: peerId, activity: activity))
}
return disposable
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,69 @@
import SwiftSignalKit
import Postbox
import TelegramApi
public enum RequirementToContact {
case premium
case stars(StarsAmount)
}
internal func _internal_updateIsPremiumRequiredToContact(account: Account, peerIds: [EnginePeer.Id]) -> Signal<[EnginePeer.Id: RequirementToContact], NoError> {
return account.postbox.transaction { transaction -> ([Api.InputUser], [PeerId]) in
var inputUsers: [Api.InputUser] = []
var ids: [PeerId] = []
for id in peerIds {
if let peer = transaction.getPeer(id), let inputUser = apiInputUser(peer) {
if peer.isPremium {
if let cachedData = transaction.getPeerCachedData(peerId: id) as? CachedUserData {
if let _ = cachedData.sendPaidMessageStars {
inputUsers.append(inputUser)
ids.append(id)
} else if cachedData.flags.contains(.premiumRequired) {
inputUsers.append(inputUser)
ids.append(id)
}
} else if let peer = peer as? TelegramUser, peer.flags.contains(.requirePremium) || peer.flags.contains(.requireStars), !peer.flags.contains(.mutualContact) {
inputUsers.append(inputUser)
ids.append(id)
}
}
}
}
return (inputUsers, ids)
} |> mapToSignal { inputUsers, reqIds -> Signal<[EnginePeer.Id: RequirementToContact], NoError> in
if !inputUsers.isEmpty {
return account.network.request(Api.functions.users.getRequirementsToContact(id: inputUsers))
|> retryRequest
|> mapToSignal { result in
return account.postbox.transaction { transaction in
var requirements: [EnginePeer.Id: RequirementToContact] = [:]
for (i, req) in result.enumerated() {
let peerId = reqIds[i]
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, cachedData in
let data = cachedData as? CachedUserData ?? CachedUserData()
var flags = data.flags
var sendPaidMessageStars = data.sendPaidMessageStars
switch req {
case .requirementToContactEmpty:
flags.remove(.premiumRequired)
sendPaidMessageStars = nil
case .requirementToContactPremium:
flags.insert(.premiumRequired)
sendPaidMessageStars = nil
requirements[peerId] = .premium
case let .requirementToContactPaidMessages(starsAmount):
flags.remove(.premiumRequired)
sendPaidMessageStars = StarsAmount(value: starsAmount, nanos: 0)
requirements[peerId] = .stars(StarsAmount(value: starsAmount, nanos: 0))
}
return data.withUpdatedFlags(flags).withUpdatedSendPaidMessageStars(sendPaidMessageStars)
})
}
return requirements
}
}
} else {
return .single([:])
}
}
}
@@ -0,0 +1,147 @@
import Foundation
import Postbox
import TelegramApi
private enum MessagePreParsingError: Error {
case invalidChatState
case malformedData
case protocolViolation
}
func processSecretChatIncomingEncryptedOperations(transaction: Transaction, peerId: PeerId) -> Bool {
if let state = transaction.getPeerChatState(peerId) as? SecretChatState {
var updatedState = state
var removeTagLocalIndices: [Int32] = []
var addedDecryptedOperations = false
transaction.operationLogEnumerateEntries(peerId: peerId, tag: OperationLogTags.SecretIncomingEncrypted, { entry in
if let operation = entry.contents as? SecretChatIncomingEncryptedOperation {
if let key = updatedState.keychain.key(fingerprint: operation.keyFingerprint) {
var decryptedContents = withDecryptedMessageContents(parameters: SecretChatEncryptionParameters(key: key, mode: .v2(role: updatedState.role)), data: operation.contents)
if decryptedContents == nil {
decryptedContents = withDecryptedMessageContents(parameters: SecretChatEncryptionParameters(key: key, mode: .v1), data: operation.contents)
}
if let decryptedContents = decryptedContents {
withExtendedLifetime(decryptedContents, {
let buffer = BufferReader(Buffer(bufferNoCopy: decryptedContents))
do {
guard let topLevelSignature = buffer.readInt32() else {
throw MessagePreParsingError.malformedData
}
let parsedLayer: Int32
let sequenceInfo: SecretChatOperationSequenceInfo?
if topLevelSignature == 0x1be31789 {
guard let _ = parseBytes(buffer) else {
throw MessagePreParsingError.malformedData
}
guard let layerValue = buffer.readInt32() else {
throw MessagePreParsingError.malformedData
}
guard let seqInValue = buffer.readInt32() else {
throw MessagePreParsingError.malformedData
}
guard let seqOutValue = buffer.readInt32() else {
throw MessagePreParsingError.malformedData
}
switch updatedState.role {
case .creator:
if seqOutValue < 0 || (seqInValue >= 0 && (seqInValue & 1) == 0) || (seqOutValue & 1) != 0 {
throw MessagePreParsingError.protocolViolation
}
case .participant:
if seqOutValue < 0 || (seqInValue >= 0 && (seqInValue & 1) != 0) || (seqOutValue & 1) == 0 {
throw MessagePreParsingError.protocolViolation
}
}
sequenceInfo = SecretChatOperationSequenceInfo(topReceivedOperationIndex: seqInValue / 2, operationIndex: seqOutValue / 2)
if layerValue == 17 {
parsedLayer = 46
} else {
parsedLayer = layerValue
}
} else {
parsedLayer = 8
sequenceInfo = nil
buffer.reset()
}
guard let messageContents = buffer.readBuffer(decryptedContents.length - Int(buffer.offset)) else {
throw MessagePreParsingError.malformedData
}
let entryTagLocalIndex: StorePeerOperationLogEntryTagLocalIndex
switch updatedState.embeddedState {
case .terminated:
throw MessagePreParsingError.invalidChatState
case .handshake:
throw MessagePreParsingError.invalidChatState
case .basicLayer:
if parsedLayer >= 46 {
guard let sequenceInfo = sequenceInfo else {
throw MessagePreParsingError.protocolViolation
}
let sequenceBasedLayerState = SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: secretChatCommonSupportedLayer(remoteLayer: parsedLayer), locallyRequestedLayer: nil, remotelyRequestedLayer: nil), rekeyState: nil, baseIncomingOperationIndex: entry.tagLocalIndex, baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil)
updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceBasedLayerState))
transaction.setPeerChatState(peerId, state: updatedState)
entryTagLocalIndex = .manual(sequenceBasedLayerState.baseIncomingOperationIndex + sequenceInfo.operationIndex)
} else {
if parsedLayer != 8 && parsedLayer != 17 {
throw MessagePreParsingError.protocolViolation
}
entryTagLocalIndex = .automatic
}
case let .sequenceBasedLayer(sequenceState):
if parsedLayer < 46 {
throw MessagePreParsingError.protocolViolation
}
entryTagLocalIndex = .manual(sequenceState.baseIncomingOperationIndex + sequenceInfo!.operationIndex)
}
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretIncomingDecrypted, tagLocalIndex: entryTagLocalIndex, tagMergedIndex: .none, contents: SecretChatIncomingDecryptedOperation(timestamp: operation.timestamp, layer: parsedLayer, sequenceInfo: sequenceInfo, contents: MemoryBuffer(messageContents), file: operation.mediaFileReference))
addedDecryptedOperations = true
} catch let error {
if let error = error as? MessagePreParsingError {
switch error {
case .invalidChatState:
break
case .malformedData, .protocolViolation:
break
}
}
Logger.shared.log("SecretChat", "peerId \(peerId) malformed data after decryption")
}
removeTagLocalIndices.append(entry.tagLocalIndex)
})
} else {
Logger.shared.log("SecretChat", "peerId \(peerId) couldn't decrypt message content")
removeTagLocalIndices.append(entry.tagLocalIndex)
}
} else {
Logger.shared.log("SecretChat", "peerId \(peerId) key \(operation.keyFingerprint) doesn't exist")
}
} else {
assertionFailure()
}
return true
})
for index in removeTagLocalIndices {
let removed = transaction.operationLogRemoveEntry(peerId: peerId, tag: OperationLogTags.SecretIncomingEncrypted, tagLocalIndex: index)
assert(removed)
}
return addedDecryptedOperations
} else {
return false
}
}
@@ -0,0 +1,158 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
func _internal_resetAccountState(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal<Never, NoError> {
return network.request(Api.functions.updates.getState())
|> retryRequest
|> mapToSignal { state -> Signal<Never, NoError> in
let chatList = fetchChatList(accountPeerId: accountPeerId, postbox: postbox, network: network, location: .general, upperBound: .absoluteUpperBound(), hash: 0, limit: 100)
return chatList
|> mapToSignal { fetchedChats -> Signal<Never, NoError> in
guard let fetchedChats = fetchedChats else {
return .never()
}
return withResolvedAssociatedMessages(postbox: postbox, source: .network(network), accountPeerId: accountPeerId, parsedPeers: fetchedChats.peers, storeMessages: fetchedChats.storeMessages, resolveThreads: false, { transaction, additionalPeers, additionalMessages -> Void in
for peerId in transaction.chatListGetAllPeerIds() {
if peerId.namespace != Namespaces.Peer.SecretChat {
transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded)
}
if peerId.namespace != Namespaces.Peer.SecretChat {
transaction.addHole(peerId: peerId, threadId: nil, namespace: Namespaces.Message.Cloud, space: .everywhere, range: 1 ... (Int32.max - 1))
}
if peerId.namespace == Namespaces.Peer.CloudChannel {
if let channel = transaction.getPeer(peerId) as? TelegramChannel, channel.isForumOrMonoForum {
transaction.setPeerPinnedThreads(peerId: peerId, threadIds: [])
for threadId in transaction.setMessageHistoryThreads(peerId: peerId) {
transaction.setMessageHistoryThreadInfo(peerId: peerId, threadId: threadId, info: nil)
transaction.addHole(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, space: .everywhere, range: 1 ... (Int32.max - 1))
}
}
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, _ in nil })
transaction.setPeerThreadCombinedState(peerId: peerId, state: nil)
}
}
transaction.removeAllChatListEntries(groupId: .root, exceptPeerNamespace: Namespaces.Peer.SecretChat)
transaction.removeAllChatListEntries(groupId: .group(1), exceptPeerNamespace: Namespaces.Peer.SecretChat)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: fetchedChats.peers.union(with: additionalPeers))
for (threadMessageId, data) in fetchedChats.threadInfos {
if let entry = StoredMessageHistoryThreadInfo(data.data) {
transaction.setMessageHistoryThreadInfo(peerId: threadMessageId.peerId, threadId: threadMessageId.threadId, info: entry)
}
transaction.replaceMessageTagSummary(peerId: threadMessageId.peerId, threadId: threadMessageId.threadId, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, customTag: nil, count: data.unreadMentionCount, maxId: data.topMessageId)
transaction.replaceMessageTagSummary(peerId: threadMessageId.peerId, threadId: threadMessageId.threadId, tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud, customTag: nil, count: data.unreadReactionCount, maxId: data.topMessageId)
}
transaction.updateCurrentPeerNotificationSettings(fetchedChats.notificationSettings)
let _ = transaction.addMessages(fetchedChats.storeMessages, location: .UpperHistoryBlock)
let _ = transaction.addMessages(additionalMessages, location: .Random)
transaction.resetIncomingReadStates(fetchedChats.readStates)
for (peerId, autoremoveValue) in fetchedChats.ttlPeriods {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if peerId.namespace == Namespaces.Peer.CloudUser {
let current = (current as? CachedUserData) ?? CachedUserData()
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
let current = (current as? CachedChannelData) ?? CachedChannelData()
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
let current = (current as? CachedGroupData) ?? CachedGroupData()
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else {
return current
}
})
}
for (peerId, value) in fetchedChats.viewForumAsMessages {
if value {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if peerId.namespace == Namespaces.Peer.CloudChannel {
let current = (current as? CachedChannelData) ?? CachedChannelData()
return current.withUpdatedViewForumAsMessages(.known(value))
} else {
return current
}
})
}
}
for hole in transaction.allChatListHoles(groupId: .root) {
transaction.replaceChatListHole(groupId: .root, index: hole.index, hole: nil)
}
for hole in transaction.allChatListHoles(groupId: .group(1)) {
transaction.replaceChatListHole(groupId: .group(1), index: hole.index, hole: nil)
}
if let hole = fetchedChats.lowerNonPinnedIndex.flatMap(ChatListHole.init) {
transaction.addChatListHole(groupId: .root, hole: hole)
}
transaction.addChatListHole(groupId: .group(1), hole: ChatListHole(index: MessageIndex(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(0)), namespace: Namespaces.Message.Cloud, id: 1), timestamp: Int32.max - 1)))
for peerId in fetchedChats.chatPeerIds {
if let peer = transaction.getPeer(peerId) {
transaction.updatePeerChatListInclusion(peerId, inclusion: .ifHasMessagesOrOneOf(groupId: .root, pinningIndex: transaction.getPeerChatListIndex(peerId)?.1.pinningIndex, minTimestamp: minTimestampForPeerInclusion(peer)))
} else {
assertionFailure()
}
}
for (peerId, peerGroupId) in fetchedChats.peerGroupIds {
if let peer = transaction.getPeer(peerId) {
transaction.updatePeerChatListInclusion(peerId, inclusion: .ifHasMessagesOrOneOf(groupId: peerGroupId, pinningIndex: nil, minTimestamp: minTimestampForPeerInclusion(peer)))
} else {
assertionFailure()
}
}
for (peerId, pts) in fetchedChats.channelStates {
if let current = transaction.getPeerChatState(peerId) as? ChannelState {
transaction.setPeerChatState(peerId, state: current.withUpdatedPts(pts))
} else {
transaction.setPeerChatState(peerId, state: ChannelState(pts: pts, invalidatedPts: nil, synchronizedUntilMessageId: nil))
}
}
if let replacePinnedItemIds = fetchedChats.pinnedItemIds {
transaction.setPinnedItemIds(groupId: .root, itemIds: replacePinnedItemIds.map(PinnedItemId.peer))
}
for (peerId, summary) in fetchedChats.mentionTagSummaries {
transaction.replaceMessageTagSummary(peerId: peerId, threadId: nil, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, customTag: nil, count: summary.count, maxId: summary.range.maxId)
}
for (peerId, summary) in fetchedChats.reactionTagSummaries {
transaction.replaceMessageTagSummary(peerId: peerId, threadId: nil, tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud, customTag: nil, count: summary.count, maxId: summary.range.maxId)
}
for (groupId, summary) in fetchedChats.folderSummaries {
transaction.resetPeerGroupSummary(groupId: groupId, namespace: Namespaces.Message.Cloud, summary: summary)
}
let savedMessageTags = transaction.getMessageTagSummaryCustomTags(peerId: accountPeerId, threadId: nil, tagMask: [], namespace: Namespaces.Message.Cloud)
if !savedMessageTags.isEmpty {
for tag in savedMessageTags {
transaction.replaceMessageTagSummary(peerId: accountPeerId, threadId: nil, tagMask: [], namespace: Namespaces.Message.Cloud, customTag: tag, count: 0, maxId: 1)
}
transaction.invalidateMessageHistoryTagsSummary(peerId: accountPeerId, threadId: nil, namespace: Namespaces.Message.Cloud, tagMask: [], customTag: savedMessageTags[0])
}
transaction.reindexUnreadCounters()
if let currentState = transaction.getState() as? AuthorizedAccountState {
switch state {
case let .state(pts, qts, date, seq, _):
transaction.setState(currentState.changedState(AuthorizedAccountState.State(pts: pts, qts: qts, date: date, seq: seq)))
}
}
})
|> ignoreValues
}
}
}
@@ -0,0 +1,246 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
public final class SavedMessageTags: Equatable, Codable {
public final class Tag: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case reaction
case title
case count
}
public let reaction: MessageReaction.Reaction
public let title: String?
public let count: Int
public init(
reaction: MessageReaction.Reaction,
title: String?,
count: Int
) {
self.reaction = reaction
self.title = title
self.count = count
}
public static func ==(lhs: Tag, rhs: Tag) -> Bool {
if lhs.reaction != rhs.reaction {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.count != rhs.count {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.reaction = try container.decode(MessageReaction.Reaction.self, forKey: .reaction)
self.title = try container.decodeIfPresent(String.self, forKey: .title)
self.count = Int(try container.decode(Int32.self, forKey: .count))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.reaction, forKey: .reaction)
try container.encodeIfPresent(self.title, forKey: .title)
try container.encode(Int32(self.count), forKey: .count)
}
}
private enum CodingKeys: String, CodingKey {
case newHash
case tags
}
public let hash: Int64
public let tags: [Tag]
public init(
hash: Int64,
tags: [Tag]
) {
self.hash = hash
self.tags = tags
}
public static func ==(lhs: SavedMessageTags, rhs: SavedMessageTags) -> Bool {
if lhs.hash != rhs.hash {
return false
}
if lhs.tags != rhs.tags {
return false
}
return true
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.hash = try container.decodeIfPresent(Int64.self, forKey: .newHash) ?? 0
self.tags = try container.decode([Tag].self, forKey: .tags)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.hash, forKey: .newHash)
try container.encode(self.tags, forKey: .tags)
}
}
func _internal_savedMessageTags(postbox: Postbox) -> Signal<SavedMessageTags?, NoError> {
return postbox.transaction { transaction -> SavedMessageTags? in
return _internal_savedMessageTags(transaction: transaction)
}
}
func _internal_savedMessageTagsCacheKey() -> ItemCacheEntryId {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.savedMessageTags, key: key)
}
func _internal_savedMessageTags(transaction: Transaction) -> SavedMessageTags? {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.savedMessageTags, key: key))?.get(SavedMessageTags.self)
if let cached = cached {
return cached
} else {
return nil
}
}
func _internal_setSavedMessageTags(transaction: Transaction, savedMessageTags: SavedMessageTags) {
let key = ValueBoxKey(length: 8)
key.setInt64(0, value: 0)
if let entry = CodableEntry(savedMessageTags) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.savedMessageTags, key: key), entry: entry)
}
}
func managedSynchronizeSavedMessageTags(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal<Never, NoError> {
let poll = Signal<Never, NoError> { subscriber in
let key: PostboxViewKey = .pendingMessageActions(type: .updateReaction)
let waitForApplySignal: Signal<Never, NoError> = postbox.combinedView(keys: [key])
|> map { views -> Bool in
guard let view = views.views[key] as? PendingMessageActionsView else {
return false
}
for entry in view.entries {
if entry.id.peerId == accountPeerId {
return false
}
}
return true
}
|> filter { $0 }
|> take(1)
|> ignoreValues
let signal: Signal<Never, NoError> = _internal_savedMessageTags(postbox: postbox)
|> mapToSignal { current in
return (network.request(Api.functions.messages.getSavedReactionTags(flags: 0, peer: nil, hash: current?.hash ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.SavedReactionTags?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Never, NoError> in
guard let result = result else {
return .complete()
}
switch result {
case .savedReactionTagsNotModified:
return .complete()
case let .savedReactionTags(tags, hash):
var customFileIds: [Int64] = []
var parsedTags: [SavedMessageTags.Tag] = []
for tag in tags {
switch tag {
case let .savedReactionTag(_, reaction, title, count):
guard let reaction = MessageReaction.Reaction(apiReaction: reaction) else {
continue
}
parsedTags.append(SavedMessageTags.Tag(
reaction: reaction,
title: title,
count: Int(count)
))
if case let .custom(fileId) = reaction {
customFileIds.append(fileId)
}
}
}
let savedMessageTags = SavedMessageTags(
hash: hash,
tags: parsedTags
)
return _internal_resolveInlineStickers(postbox: postbox, network: network, fileIds: customFileIds)
|> mapToSignal { _ -> Signal<Never, NoError> in
return postbox.transaction { transaction in
_internal_setSavedMessageTags(transaction: transaction, savedMessageTags: savedMessageTags)
}
|> ignoreValues
}
}
})
}
return (waitForApplySignal |> then(signal)).start(completed: {
subscriber.putCompletion()
})
}
return (
poll
|> then(
.complete()
|> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue())
)
)
|> restart
}
func _internal_setSavedMessageTagTitle(account: Account, reaction: MessageReaction.Reaction, title: String?) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
let value = _internal_savedMessageTags(transaction: transaction) ?? SavedMessageTags(hash: 0, tags: [])
var updatedTags = value.tags
if let index = updatedTags.firstIndex(where: { $0.reaction == reaction }) {
updatedTags[index] = SavedMessageTags.Tag(reaction: updatedTags[index].reaction, title: title, count: updatedTags[index].count)
} else {
updatedTags.append(SavedMessageTags.Tag(reaction: reaction, title: title, count: 0))
}
_internal_setSavedMessageTags(transaction: transaction, savedMessageTags: SavedMessageTags(hash: 0, tags: updatedTags))
}
|> mapToSignal { _ -> Signal<Never, NoError> in
var flags: Int32 = 0
if title != nil {
flags |= 1 << 0
}
return account.network.request(Api.functions.messages.updateSavedReactionTag(flags: flags, reaction: reaction.apiReaction, title: title))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> ignoreValues
}
}
@@ -0,0 +1,285 @@
import Foundation
import MtProtoKit
import TelegramApi
private let apiPrefix: String = {
let type = _typeName(Api.User.self)
let userType = "User"
if type.hasSuffix(userType) {
return String(type[type.startIndex ..< type.index(type.endIndex, offsetBy: -userType.count)])
} else {
return "TelegramApi.Api."
}
}()
private let apiPrefixLength = apiPrefix.count
private let redactChildrenOfType: [String: Set<String>] = [
"Message.message": Set(["message"]),
"Updates.updateShortMessage": Set(["message"]),
"Updates.updateShortChatMessage": Set(["message"]),
"BotInlineMessage.botInlineMessageText": Set(["message"]),
"DraftMessage.draftMessage": Set(["message"]),
"InputSingleMedia.inputSingleMedia": Set(["message"]),
"InputContact.inputPhoneContact": Set(["phone"]),
"User.user": Set(["phone"]),
"Update.updateUserPhone": Set(["phone"])
]
private let redactFunctionParameters: [String: Set<String>] = [
"messages.sendMessage": Set(["message"]),
"messages.sendMedia": Set(["message"]),
"messages.saveDraft": Set(["message"]),
"messages.getWebPagePreview": Set(["message"])
]
func apiFunctionDescription(of desc: FunctionDescription) -> String {
var result = desc.name
if !desc.parameters.isEmpty {
result.append("(")
var first = true
for param in desc.parameters {
if first {
first = false
} else {
result.append(", ")
}
result.append(param.0)
result.append(": ")
var redactParam = false
if let redactParams = redactFunctionParameters[desc.name] {
redactParam = redactParams.contains(param.0)
}
if redactParam, Logger.shared.redactSensitiveData {
result.append("[[redacted]]")
} else {
result.append(recursiveDescription(redact: Logger.shared.redactSensitiveData, of: param.1))
}
}
result.append(")")
}
return result
}
func apiShortFunctionDescription(of desc: FunctionDescription) -> String {
return desc.name
}
private func recursiveDescription(redact: Bool, of value: Any) -> String {
let mirror = Mirror(reflecting: value)
var result = ""
if let displayStyle = mirror.displayStyle {
switch displayStyle {
case .enum:
result.append(_typeName(mirror.subjectType))
if result.hasPrefix(apiPrefix) {
result.removeSubrange(result.startIndex ..< result.index(result.startIndex, offsetBy: apiPrefixLength))
}
if let value = value as? TypeConstructorDescription {
let (consName, fields) = value.descriptionFields()
result.append(".")
result.append(consName)
let redactChildren: Set<String>?
if redact {
redactChildren = redactChildrenOfType[result]
} else {
redactChildren = nil
}
if !fields.isEmpty {
result.append("(")
var first = true
for (fieldName, fieldValue) in fields {
if first {
first = false
} else {
result.append(", ")
}
var redactValue: Bool = false
if let redactChildren = redactChildren, redactChildren.contains("*") {
redactValue = true
}
result.append(fieldName)
result.append(": ")
if let redactChildren = redactChildren, redactChildren.contains(fieldName) {
redactValue = true
}
if redactValue {
result.append("[[redacted]]")
} else {
result.append(recursiveDescription(redact: redact, of: fieldValue))
}
}
result.append(")")
}
} else {
inner: for child in mirror.children {
if let label = child.label {
result.append(".")
result.append(label)
}
let redactChildren: Set<String>?
if redact {
redactChildren = redactChildrenOfType[result]
} else {
redactChildren = nil
}
let valueMirror = Mirror(reflecting: child.value)
if let displayStyle = valueMirror.displayStyle {
switch displayStyle {
case .tuple:
var hadChildren = false
for child in valueMirror.children {
if !hadChildren {
hadChildren = true
result.append("(")
} else {
result.append(", ")
}
var redactValue: Bool = false
if let redactChildren = redactChildren, redactChildren.contains("*") {
redactValue = true
}
if let label = child.label {
result.append(label)
result.append(": ")
if let redactChildren = redactChildren, redactChildren.contains(label) {
redactValue = true
}
}
if redactValue {
result.append("[[redacted]]")
} else {
result.append(recursiveDescription(redact: redact, of: child.value))
}
}
if hadChildren {
result.append(")")
}
default:
break
}
} else {
result.append("(")
result.append(String(describing: child.value))
result.append(")")
}
break
}
}
case .collection:
result.append("[")
var isFirst = true
for child in mirror.children {
if isFirst {
isFirst = false
} else {
result.append(", ")
}
result.append(recursiveDescription(redact: redact, of: child.value))
}
result.append("]")
default:
result.append("\(value)")
}
} else {
result.append("\(value)")
}
return result
}
public class BoxedMessage: NSObject {
public let body: Any
public init(_ body: Any) {
self.body = body
}
override public var description: String {
get {
return recursiveDescription(redact: Logger.shared.redactSensitiveData, of: self.body)
}
}
}
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 220
}
public func parseMessage(_ data: Data!) -> Any! {
if let body = Api.parse(Buffer(data: data)) {
return BoxedMessage(body)
}
return nil
}
public func exportAuthorization(_ datacenterId: Int32, data: AutoreleasingUnsafeMutablePointer<NSData?>) -> MTExportAuthorizationResponseParser!
{
let functionContext = Api.functions.auth.exportAuthorization(dcId: datacenterId)
data.pointee = functionContext.1.makeData() as NSData
return { data -> MTExportedAuthorizationData? in
if let exported = functionContext.2.parse(Buffer(data: data)) {
switch exported {
case let .exportedAuthorization(id, bytes):
return MTExportedAuthorizationData(authorizationBytes: bytes.makeData(), authorizationId: id)
}
} else {
return nil
}
}
}
public func importAuthorization(_ authId: Int64, bytes: Data!) -> Data! {
return Api.functions.auth.importAuthorization(id: authId, bytes: Buffer(data: bytes)).1.makeData()
}
public func requestDatacenterAddress(with data: AutoreleasingUnsafeMutablePointer<NSData?>) -> MTRequestDatacenterAddressListParser! {
let (_, buffer, parser) = Api.functions.help.getConfig()
data.pointee = buffer.makeData() as NSData
return { response -> MTDatacenterAddressListData? in
if let config = parser.parse(Buffer(data: response)) {
switch config {
case let .config(_, _, _, _, _, dcOptions, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
var addressDict: [NSNumber: [Any]] = [:]
for option in dcOptions {
switch option {
case let .dcOption(flags, id, ipAddress, port, secret):
if addressDict[id as NSNumber] == nil {
addressDict[id as NSNumber] = []
}
let preferForMedia = (flags & (1 << 1)) != 0
let restrictToTcp = (flags & (1 << 2)) != 0
let isCdn = (flags & (1 << 3)) != 0
let preferForProxy = (flags & (1 << 4)) != 0
addressDict[id as NSNumber]!.append(MTDatacenterAddress(ip: ipAddress, port: UInt16(port), preferForMedia: preferForMedia, restrictToTcp: restrictToTcp, cdn: isCdn, preferForProxy: preferForProxy, secret: secret?.makeData()))
break
}
}
return MTDatacenterAddressListData(addressList: addressDict)
}
}
return nil
}
}
public func requestNoop(_ data: AutoreleasingUnsafeMutablePointer<NSData?>!) -> MTRequestNoopParser! {
let (_, buffer, parser) = Api.functions.help.test()
data.pointee = buffer.makeData() as NSData
return { response -> AnyObject? in
if let _ = parser.parse(Buffer(data: response)) {
return true as NSNumber
} else {
return nil
}
}
}
}
@@ -0,0 +1,358 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
enum FeaturedStickerPacksCategory {
case stickerPacks
case emojiPacks
}
extension FeaturedStickerPacksCategory {
var itemListNamespace: Int32 {
switch self {
case .stickerPacks:
return Namespaces.OrderedItemList.CloudFeaturedStickerPacks
case .emojiPacks:
return Namespaces.OrderedItemList.CloudFeaturedEmojiPacks
}
}
var collectionIdNamespace: Int32 {
switch self {
case .stickerPacks:
return Namespaces.ItemCollection.CloudStickerPacks
case .emojiPacks:
return Namespaces.ItemCollection.CloudEmojiPacks
}
}
}
private func hashForIdsReverse(_ ids: [Int64]) -> Int64 {
var acc: UInt64 = 0
for id in ids {
combineInt64Hash(&acc, with: UInt64(bitPattern: id))
}
return Int64(bitPattern: acc)
}
private func hashForIdsReverse(_ ids: [Int64], unreadIds: [Int64]) -> Int64 {
var acc: UInt64 = 0
for id in ids {
combineInt64Hash(&acc, with: UInt64(bitPattern: id))
if unreadIds.contains(id) {
combineInt64Hash(&acc, with: 1 as UInt64)
}
}
return Int64(bitPattern: acc)
}
func manageStickerPacks(network: Network, postbox: Postbox) -> Signal<Void, NoError> {
return (postbox.transaction { transaction -> Void in
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .stickers, content: .sync, noDelay: false)
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .masks, content: .sync, noDelay: false)
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .emoji, content: .sync, noDelay: false)
addSynchronizeSavedGifsOperation(transaction: transaction, operation: .sync)
addSynchronizeSavedStickersOperation(transaction: transaction, operation: .sync)
addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .sync)
} |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func resolveMissingStickerSets(network: Network, postbox: Postbox, stickerSets: [Api.StickerSetCovered], ignorePacksWithHashes: [Int64: Int32]) -> Signal<[Api.StickerSetCovered], NoError> {
var missingSignals: [Signal<(Int, Api.StickerSetCovered)?, NoError>] = []
for i in 0 ..< stickerSets.count {
switch stickerSets[i] {
case let .stickerSetNoCovered(value), let .stickerSetCovered(value, _):
switch value {
case let .stickerSet(_, _, id, accessHash, _, _, _, _, _, _, _, hash):
if ignorePacksWithHashes[id] == hash {
continue
}
missingSignals.append(network.request(Api.functions.messages.getStickerSet(stickerset: .inputStickerSetID(id: id, accessHash: accessHash), hash: 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.StickerSet?, NoError> in
return .single(nil)
}
|> map { result -> (Int, Api.StickerSetCovered)? in
if let result = result {
switch result {
case let .stickerSet(set, packs, keywords, documents):
return (i, Api.StickerSetCovered.stickerSetFullCovered(set: set, packs: packs, keywords: keywords, documents: documents))
case .stickerSetNotModified:
return nil
}
} else {
return nil
}
})
}
default:
break
}
}
return combineLatest(missingSignals)
|> map { results -> [Api.StickerSetCovered] in
var updatedSets = stickerSets
for result in results {
if let result = result {
updatedSets[result.0] = result.1
}
}
return updatedSets
}
}
func updatedFeaturedStickerPacks(network: Network, postbox: Postbox, category: FeaturedStickerPacksCategory) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
let initialPacks = transaction.getOrderedListItems(collectionId: category.itemListNamespace)
var initialPackMap: [Int64: FeaturedStickerPackItem] = [:]
var unreadIds: [Int64] = []
for entry in initialPacks {
let item = entry.contents.get(FeaturedStickerPackItem.self)!
initialPackMap[FeaturedStickerPackItemId(entry.id).packId] = item
if item.unread {
unreadIds.append(item.info.id.id)
}
}
let initialPackIds = initialPacks.map {
return FeaturedStickerPackItemId($0.id).packId
}
let initialHash: Int64 = hashForIdsReverse(initialPackIds, unreadIds: unreadIds)
struct FeaturedListContent {
var unreadIds: Set<Int64>
var packs: [FeaturedStickerPackItem]
var isPremium: Bool
}
enum FeaturedList {
case notModified
case content(FeaturedListContent)
}
let signal: Signal<FeaturedList, NoError>
switch category {
case .stickerPacks:
signal = network.request(Api.functions.messages.getFeaturedStickers(hash: initialHash))
|> mapToSignal { result -> Signal<FeaturedList, MTRpcError> in
switch result {
case .featuredStickersNotModified:
return .single(.notModified)
case let .featuredStickers(flags, _, _, sets, unread):
return resolveMissingStickerSets(network: network, postbox: postbox, stickerSets: sets, ignorePacksWithHashes: initialPackMap.filter { $0.value.topItems.count > 1 }.mapValues({ item in
item.info.hash
}))
|> castError(MTRpcError.self)
|> map { sets -> FeaturedList in
let unreadIds = Set(unread)
var updatedPacks: [FeaturedStickerPackItem] = []
for set in sets {
var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace)
if let previousPack = initialPackMap[info.id.id] {
if previousPack.info.hash == info.hash, previousPack.topItems.count > 1 {
items = previousPack.topItems
} else {
items = Array(items.prefix(5))
}
}
updatedPacks.append(FeaturedStickerPackItem(info: StickerPackCollectionInfo.Accessor(info), topItems: items, unread: unreadIds.contains(info.id.id)))
}
let isPremium = flags & (1 << 0) != 0
return .content(FeaturedListContent(
unreadIds: unreadIds,
packs: updatedPacks,
isPremium: isPremium
))
}
}
}
|> `catch` { _ -> Signal<FeaturedList, NoError> in
return .single(.notModified)
}
case .emojiPacks:
signal = network.request(Api.functions.messages.getFeaturedEmojiStickers(hash: initialHash))
|> mapToSignal { result -> Signal<FeaturedList, MTRpcError> in
switch result {
case .featuredStickersNotModified:
return .single(.notModified)
case let .featuredStickers(flags, _, _, sets, unread):
return resolveMissingStickerSets(network: network, postbox: postbox, stickerSets: sets, ignorePacksWithHashes: initialPackMap.mapValues({ item in
item.info.hash
}))
|> castError(MTRpcError.self)
|> map { sets -> FeaturedList in
let unreadIds = Set(unread)
var updatedPacks: [FeaturedStickerPackItem] = []
for set in sets {
var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace)
if let previousPack = initialPackMap[info.id.id] {
if previousPack.info.hash == info.hash {
items = previousPack.topItems
}
}
updatedPacks.append(FeaturedStickerPackItem(info: StickerPackCollectionInfo.Accessor(info), topItems: items, unread: unreadIds.contains(info.id.id)))
}
let isPremium = flags & (1 << 0) != 0
return .content(FeaturedListContent(
unreadIds: unreadIds,
packs: updatedPacks,
isPremium: isPremium
))
}
}
}
|> `catch` { _ -> Signal<FeaturedList, NoError> in
return .single(.notModified)
}
}
return signal
|> mapToSignal { result -> Signal<Void, NoError> in
return postbox.transaction { transaction -> Void in
switch result {
case .notModified:
break
case let .content(content):
transaction.replaceOrderedItemListItems(collectionId: category.itemListNamespace, items: content.packs.compactMap { item -> OrderedItemListEntry? in
if let entry = CodableEntry(item) {
return OrderedItemListEntry(id: FeaturedStickerPackItemId(item.info.id.id).rawValue, contents: entry)
} else {
return nil
}
})
if let entry = CodableEntry(FeaturedStickersConfiguration(isPremium: content.isPremium)) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, key: ValueBoxKey(length: 0)), entry: entry)
}
}
}
}
}
|> switchToLatest
}
public func requestOldFeaturedStickerPacks(network: Network, postbox: Postbox, offset: Int, limit: Int) -> Signal<[FeaturedStickerPackItem], NoError> {
return network.request(Api.functions.messages.getOldFeaturedStickers(offset: Int32(offset), limit: Int32(limit), hash: 0))
|> retryRequest
|> map { result -> [FeaturedStickerPackItem] in
switch result {
case .featuredStickersNotModified:
return []
case let .featuredStickers(_, _, _, sets, unread):
let unreadIds = Set(unread)
var updatedPacks: [FeaturedStickerPackItem] = []
for set in sets {
let (info, items) = parsePreviewStickerSet(set, namespace: Namespaces.ItemCollection.CloudStickerPacks)
updatedPacks.append(FeaturedStickerPackItem(info: StickerPackCollectionInfo.Accessor(info), topItems: items, unread: unreadIds.contains(info.id.id)))
}
return updatedPacks
}
}
}
public func preloadedFeaturedStickerSet(network: Network, postbox: Postbox, id: ItemCollectionId) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Signal<Void, NoError> in
if let pack = transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks, itemId: FeaturedStickerPackItemId(id.id).rawValue)?.contents.get(FeaturedStickerPackItem.self) {
if pack.topItems.count < 5 && pack.topItems.count < pack.info.count {
return _internal_requestStickerSet(postbox: postbox, network: network, reference: .id(id: pack.info.id.id, accessHash: pack.info.accessHash))
|> map(Optional.init)
|> `catch` { _ -> Signal<RequestStickerSetResult?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
return postbox.transaction { transaction -> Void in
if let pack = transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks, itemId: FeaturedStickerPackItemId(id.id).rawValue)?.contents.get(FeaturedStickerPackItem.self) {
var items = result.items.map({ $0 as? StickerPackItem }).compactMap({ $0 })
if items.count > 5 {
items.removeSubrange(5 ..< items.count)
}
if let entry = CodableEntry(FeaturedStickerPackItem(info: pack.info, topItems: items, unread: pack.unread)) {
transaction.updateOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks, itemId: FeaturedStickerPackItemId(id.id).rawValue, item: entry)
}
}
}
} else {
return .complete()
}
}
}
}
return .complete()
} |> switchToLatest
}
func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollectionId.Namespace) -> (StickerPackCollectionInfo, [StickerPackItem]) {
switch set {
case let .stickerSetCovered(set, cover):
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var items: [StickerPackItem] = []
if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id {
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []))
}
return (info, items)
case let .stickerSetMultiCovered(set, covers):
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var items: [StickerPackItem] = []
for cover in covers {
if let file = telegramMediaFileFromApiDocument(cover, altDocuments: []), let id = file.id {
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: []))
}
}
return (info, items)
case let .stickerSetFullCovered(set, packs, keywords, documents):
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs {
switch pack {
case let .stickerPack(text, fileIds):
let key = ValueBoxKey(text).toMemoryBuffer()
for fileId in fileIds {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
break
}
}
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var items: [StickerPackItem] = []
for document in documents {
if let file = telegramMediaFileFromApiDocument(document, altDocuments: []), let id = file.id {
let fileIndexKeys: [MemoryBuffer]
if let indexKeys = indexKeysByFile[id] {
fileIndexKeys = indexKeys
} else {
fileIndexKeys = []
}
items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: fileIndexKeys))
}
}
return (info, items)
case let .stickerSetNoCovered(set):
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
let items: [StickerPackItem] = []
return (info, items)
}
}
@@ -0,0 +1,34 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
public func addAppLogEvent(postbox: Postbox, time: Double = Date().timeIntervalSince1970, type: String, peerId: PeerId? = nil, data: JSON = .dictionary([:])) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeAppLogEvents
let peerId = PeerId(0)
let _ = (postbox.transaction { transaction in
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeAppLogEventsOperation(content: .add(time: time, type: type, peerId: peerId, data: data)))
}).start()
}
public func invokeAppLogEventsSynchronization(postbox: Postbox) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeAppLogEvents
let peerId = PeerId(0)
let _ = (postbox.transaction { transaction in
var topOperation: (SynchronizeSavedStickersOperation, Int32)?
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in
if let operation = entry.contents as? SynchronizeSavedStickersOperation, case .sync = operation.content {
topOperation = (operation, entry.tagLocalIndex)
}
return false
})
if let (_, topLocalIndex) = topOperation {
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex)
}
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeAppLogEventsOperation(content: .sync))
}).start()
}
@@ -0,0 +1,78 @@
import Foundation
import Postbox
import SwiftSignalKit
public final class SynchronizeAutosaveItemOperation: PostboxCoding {
public struct Content: Codable {
public var messageId: MessageId
public var mediaId: MediaId
public init(messageId: MessageId, mediaId: MediaId) {
self.messageId = messageId
self.mediaId = mediaId
}
}
public let messageId: MessageId
public let mediaId: MediaId
public init(messageId: MessageId, mediaId: MediaId) {
self.messageId = messageId
self.mediaId = mediaId
}
public init(decoder: PostboxDecoder) {
if let content = decoder.decode(Content.self, forKey: "c") {
self.messageId = content.messageId
self.mediaId = content.mediaId
} else {
self.messageId = MessageId(peerId: PeerId(0), namespace: 0, id: 0)
self.mediaId = MediaId(namespace: 0, id: 0)
}
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encode(Content(messageId: self.messageId, mediaId: self.mediaId), forKey: "c")
}
}
public func addSynchronizeAutosaveItemOperation(transaction: Transaction, messageId: MessageId, mediaId: MediaId) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeAutosaveItems
let peerId = PeerId(0)
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeAutosaveItemOperation(messageId: messageId, mediaId: mediaId))
}
public func addSynchronizeAutosaveItemOperation(postbox: Postbox, messageId: MessageId, mediaId: MediaId) -> Signal<Never, NoError> {
return postbox.transaction { transaction -> Void in
addSynchronizeAutosaveItemOperation(transaction: transaction, messageId: messageId, mediaId: mediaId)
}
|> ignoreValues
}
public func _internal_getSynchronizeAutosaveItemOperations(transaction: Transaction) -> [(index: Int32, message: Message, mediaId: MediaId)] {
let peerId = PeerId(0)
var result: [(index: Int32, message: Message, mediaId: MediaId)] = []
var removeIndices: [Int32] = []
transaction.operationLogEnumerateEntries(peerId: peerId, tag: OperationLogTags.SynchronizeAutosaveItems, { entry in
if let operation = entry.contents as? SynchronizeAutosaveItemOperation {
if let message = transaction.getMessage(operation.messageId) {
result.append((index: entry.tagLocalIndex, message: message, mediaId: operation.mediaId))
} else {
removeIndices.append(entry.tagLocalIndex)
}
}
return true
})
for index in removeIndices {
let _ = transaction.operationLogRemoveEntry(peerId: PeerId(0), tag: OperationLogTags.SynchronizeAutosaveItems, tagLocalIndex: index)
}
return result
}
public func _internal_removeSyncrhonizeAutosaveItemOperations(transaction: Transaction, indices: [Int32]) {
for index in indices {
let _ = transaction.operationLogRemoveEntry(peerId: PeerId(0), tag: OperationLogTags.SynchronizeAutosaveItems, tagLocalIndex: index)
}
}
@@ -0,0 +1,41 @@
import Foundation
import Postbox
func addSynchronizeChatInputStateOperation(transaction: Transaction, peerId: PeerId, threadId: Int64?) {
var removeLocalIndices: [Int32] = []
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeChatInputStates
var previousOperation: SynchronizeChatInputStateOperation?
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in
if let operation = entry.contents as? SynchronizeChatInputStateOperation {
if operation.threadId == threadId {
previousOperation = operation
removeLocalIndices.append(entry.tagLocalIndex)
return false
}
} else {
removeLocalIndices.append(entry.tagLocalIndex)
}
return true
})
var previousState: SynchronizeableChatInputState?
if let previousOperation = previousOperation {
previousState = previousOperation.previousState
} else {
let peerChatInterfaceState: StoredPeerChatInterfaceState?
if let threadId = threadId {
peerChatInterfaceState = transaction.getPeerChatThreadInterfaceState(peerId, threadId: threadId)
} else {
peerChatInterfaceState = transaction.getPeerChatInterfaceState(peerId)
}
if let peerChatInterfaceState = peerChatInterfaceState, let data = peerChatInterfaceState.data {
previousState = (try? AdaptedPostboxDecoder().decode(InternalChatInterfaceState.self, from: data))?.synchronizeableInputState
}
}
let operationContents = SynchronizeChatInputStateOperation(previousState: previousState, threadId: threadId)
for index in removeLocalIndices {
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: index)
}
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents)
}
@@ -0,0 +1,16 @@
import Postbox
func addSynchronizeConsumeMessageContentsOperation(transaction: Transaction, messageIds: [MessageId]) {
for (peerId, messageIds) in messagesIdsGroupedByPeerId(Set(messageIds)) {
let updateLocalIndex: Int32? = nil
/*transaction.operationLogEnumerateEntries(peerId: peerId, tag: OperationLogTags.SynchronizeConsumeMessageContents, { entry in
updateLocalIndex = entry.tagLocalIndex
return false
})*/
let operationContents = SynchronizeConsumeMessageContentsOperation(messageIds: messageIds)
if let updateLocalIndex = updateLocalIndex {
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: OperationLogTags.SynchronizeConsumeMessageContents, tagLocalIndex: updateLocalIndex)
}
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SynchronizeConsumeMessageContents, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents)
}
}
@@ -0,0 +1,21 @@
import Foundation
import Postbox
import MurMurHash32
func addSynchronizeEmojiKeywordsOperation(transaction: Transaction, inputLanguageCode: String, languageCode: String?, fromVersion: Int32?) {
let tag = OperationLogTags.SynchronizeEmojiKeywords
let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(1), id: PeerId.Id._internalFromInt64Value(Int64(abs(murMurHashString32(inputLanguageCode)))))
var hasExistingOperation = false
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag) { entry -> Bool in
hasExistingOperation = true
return false
}
guard !hasExistingOperation else {
return
}
let operationContents = SynchronizeEmojiKeywordsOperation(inputLanguageCode: inputLanguageCode, languageCode: languageCode, fromVersion: fromVersion)
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents)
}
@@ -0,0 +1,34 @@
import Foundation
import Postbox
import SwiftSignalKit
func _internal_updatePeerGroupIdInteractively(transaction: Transaction, peerId: PeerId, groupId: PeerGroupId) {
let initialInclusion = transaction.getPeerChatListInclusion(peerId)
var updatedInclusion = initialInclusion
switch initialInclusion {
case .notIncluded:
break
case let .ifHasMessagesOrOneOf(currentGroupId, pinningIndex, minTimestamp):
if currentGroupId == groupId {
return
}
if pinningIndex != nil {
/*let updatedPinnedItems = transaction.getPinnedItemIds(groupId: currentGroupId).filter({ $0 != .peer(peerId) })
transaction.setPinnedItemIds(groupId: currentGroupId, itemIds: updatedPinnedItems)*/
}
updatedInclusion = .ifHasMessagesOrOneOf(groupId: groupId, pinningIndex: nil, minTimestamp: minTimestamp)
}
if initialInclusion != updatedInclusion {
transaction.updatePeerChatListInclusion(peerId, inclusion: updatedInclusion)
if peerId.namespace != Namespaces.Peer.SecretChat {
addSynchronizeGroupedPeersOperation(transaction: transaction, peerId: peerId, groupId: groupId)
}
}
}
private func addSynchronizeGroupedPeersOperation(transaction: Transaction, peerId: PeerId, groupId: PeerGroupId) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeGroupedPeers
let logPeerId = PeerId(0)
transaction.operationLogAddEntry(peerId: logPeerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeGroupedPeersOperation(peerId: peerId, groupId: groupId))
}
@@ -0,0 +1,94 @@
import Postbox
public enum AddSynchronizeInstalledStickerPacksOperationContent {
case sync
case add([ItemCollectionId])
case remove([ItemCollectionId])
case archive([ItemCollectionId])
}
public func addSynchronizeInstalledStickerPacksOperation(transaction: Transaction, namespace: ItemCollectionId.Namespace, content: AddSynchronizeInstalledStickerPacksOperationContent, noDelay: Bool) {
let operationNamespace: SynchronizeInstalledStickerPacksOperationNamespace
switch namespace {
case Namespaces.ItemCollection.CloudStickerPacks:
operationNamespace = .stickers
case Namespaces.ItemCollection.CloudMaskPacks:
operationNamespace = .masks
case Namespaces.ItemCollection.CloudEmojiPacks:
operationNamespace = .emoji
default:
return
}
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: operationNamespace, content: content, noDelay: noDelay)
}
func addSynchronizeInstalledStickerPacksOperation(transaction: Transaction, namespace: SynchronizeInstalledStickerPacksOperationNamespace, content: AddSynchronizeInstalledStickerPacksOperationContent, noDelay: Bool) {
var updateLocalIndex: Int32?
let tag: PeerOperationLogTag
let itemCollectionNamespace: ItemCollectionId.Namespace
switch namespace {
case .stickers:
tag = OperationLogTags.SynchronizeInstalledStickerPacks
itemCollectionNamespace = Namespaces.ItemCollection.CloudStickerPacks
case .masks:
tag = OperationLogTags.SynchronizeInstalledMasks
itemCollectionNamespace = Namespaces.ItemCollection.CloudMaskPacks
case .emoji:
tag = OperationLogTags.SynchronizeInstalledEmoji
itemCollectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks
}
var previousStickerPackIds: [ItemCollectionId]?
var archivedPacks: [ItemCollectionId] = []
transaction.operationLogEnumerateEntries(peerId: PeerId(0), tag: tag, { entry in
updateLocalIndex = entry.tagLocalIndex
if let operation = entry.contents as? SynchronizeInstalledStickerPacksOperation {
previousStickerPackIds = operation.previousPacks
archivedPacks = operation.archivedPacks
} else {
assertionFailure()
}
return false
})
let previousPacks = previousStickerPackIds ?? transaction.getItemCollectionsInfos(namespace: itemCollectionNamespace).map { $0.0 }
switch content {
case .sync:
break
case let .add(ids):
let idsSet = Set(ids)
archivedPacks = archivedPacks.filter({ !idsSet.contains($0) })
case let .remove(ids):
let idsSet = Set(ids)
archivedPacks = archivedPacks.filter({ !idsSet.contains($0) })
case let .archive(ids):
for id in ids {
if !archivedPacks.contains(id) {
archivedPacks.append(id)
}
}
}
let operationContents = SynchronizeInstalledStickerPacksOperation(previousPacks: previousPacks, archivedPacks: archivedPacks, noDelay: noDelay)
if let updateLocalIndex = updateLocalIndex {
let _ = transaction.operationLogRemoveEntry(peerId: PeerId(0), tag: tag, tagLocalIndex: updateLocalIndex)
}
transaction.operationLogAddEntry(peerId: PeerId(0), tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents)
}
func addSynchronizeMarkFeaturedStickerPacksAsSeenOperation(transaction: Transaction, ids: [ItemCollectionId]) {
var updateLocalIndex: Int32?
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkFeaturedStickerPacksAsSeen
var previousIds = Set<ItemCollectionId>()
transaction.operationLogEnumerateEntries(peerId: PeerId(0), tag: tag, { entry in
updateLocalIndex = entry.tagLocalIndex
if let operation = entry.contents as? SynchronizeMarkFeaturedStickerPacksAsSeenOperation {
previousIds = Set(operation.ids)
} else {
assertionFailure()
}
return false
})
let operationContents = SynchronizeMarkFeaturedStickerPacksAsSeenOperation(ids: Array(previousIds.union(Set(ids))))
if let updateLocalIndex = updateLocalIndex {
let _ = transaction.operationLogRemoveEntry(peerId: PeerId(0), tag: tag, tagLocalIndex: updateLocalIndex)
}
transaction.operationLogAddEntry(peerId: PeerId(0), tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: operationContents)
}
@@ -0,0 +1,21 @@
import Foundation
import Postbox
import SwiftSignalKit
func addSynchronizeLocalizationUpdatesOperation(transaction: Transaction) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeLocalizationUpdates
let peerId = PeerId(0)
var topLocalIndex: Int32?
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in
topLocalIndex = entry.tagLocalIndex
return false
})
if let topLocalIndex = topLocalIndex {
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex)
}
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeLocalizationUpdatesOperation())
}
@@ -0,0 +1,48 @@
import Foundation
import Postbox
import SwiftSignalKit
func addSynchronizeMarkAllUnseenPersonalMessagesOperation(transaction: Transaction, peerId: PeerId, maxId: MessageId.Id) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkAllUnseenPersonalMessages
var topLocalIndex: Int32?
var currentMaxId: MessageId.Id?
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in
topLocalIndex = entry.tagLocalIndex
if let operation = entry.contents as? SynchronizeMarkAllUnseenPersonalMessagesOperation {
currentMaxId = operation.maxId
}
return false
})
if let topLocalIndex = topLocalIndex {
if let currentMaxId = currentMaxId, currentMaxId >= maxId {
return
}
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex)
}
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeMarkAllUnseenPersonalMessagesOperation(maxId: maxId))
}
func addSynchronizeMarkAllUnseenReactionsOperation(transaction: Transaction, peerId: PeerId, maxId: MessageId.Id, threadId: Int64?) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeMarkAllUnseenReactions
var topLocalIndex: Int32?
var currentMaxId: MessageId.Id?
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in
topLocalIndex = entry.tagLocalIndex
if let operation = entry.contents as? SynchronizeMarkAllUnseenReactionsOperation, operation.threadId == threadId {
currentMaxId = operation.maxId
}
return false
})
if let topLocalIndex = topLocalIndex {
if let currentMaxId = currentMaxId, currentMaxId >= maxId {
return
}
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex)
}
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeMarkAllUnseenReactionsOperation(threadId: threadId, maxId: maxId))
}
@@ -0,0 +1,379 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
private enum PeerReadStateMarker: Equatable {
case Global(Int32)
case Channel(Int32)
}
private func inputPeer(postbox: Postbox, peerId: PeerId) -> Signal<Api.InputPeer, PeerReadStateValidationError> {
return postbox.loadedPeerWithId(peerId)
|> mapToSignalPromotingError { peer -> Signal<Api.InputPeer, PeerReadStateValidationError> in
if let inputPeer = apiInputPeer(peer) {
return .single(inputPeer)
} else {
return .fail(.retry)
}
}
|> take(1)
}
private func inputSecretChat(postbox: Postbox, peerId: PeerId) -> Signal<Api.InputEncryptedChat, PeerReadStateValidationError> {
return postbox.loadedPeerWithId(peerId)
|> mapToSignalPromotingError { peer -> Signal<Api.InputEncryptedChat, PeerReadStateValidationError> in
if let inputPeer = apiInputSecretChat(peer) {
return .single(inputPeer)
} else {
return .fail(.retry)
}
}
|> take(1)
}
private func dialogTopMessage(network: Network, postbox: Postbox, peerId: PeerId) -> Signal<(Int32, Int32)?, PeerReadStateValidationError> {
return inputPeer(postbox: postbox, peerId: peerId)
|> mapToSignal { inputPeer -> Signal<(Int32, Int32)?, PeerReadStateValidationError> in
return network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: Int32.max, offsetDate: Int32.max, addOffset: 0, limit: 1, maxId: Int32.max, minId: 1, hash: 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.Messages?, NoError> in
return .single(nil)
}
|> mapToSignalPromotingError { result -> Signal<(Int32, Int32)?, PeerReadStateValidationError> in
guard let result = result else {
return .single(nil)
}
let apiMessages: [Api.Message]
switch result {
case let .channelMessages(_, _, _, _, messages, _, _, _):
apiMessages = messages
case let .messages(messages, _, _, _):
apiMessages = messages
case let .messagesSlice(_, _, _, _, _, messages, _, _, _):
apiMessages = messages
case .messagesNotModified:
apiMessages = []
}
if let message = apiMessages.first, let timestamp = message.timestamp {
return .single((message.rawId, timestamp))
} else {
return .single(nil)
}
}
}
}
private func dialogReadState(network: Network, postbox: Postbox, peerId: PeerId) -> Signal<(PeerReadState, PeerReadStateMarker)?, PeerReadStateValidationError> {
return dialogTopMessage(network: network, postbox: postbox, peerId: peerId)
|> mapToSignal { topMessage -> Signal<(PeerReadState, PeerReadStateMarker)?, PeerReadStateValidationError> in
guard let _ = topMessage else {
return .single(nil)
}
return inputPeer(postbox: postbox, peerId: peerId)
|> mapToSignal { inputPeer -> Signal<(PeerReadState, PeerReadStateMarker)?, PeerReadStateValidationError> in
return network.request(Api.functions.messages.getPeerDialogs(peers: [.inputDialogPeer(peer: inputPeer)]))
|> retryRequest
|> mapToSignalPromotingError { result -> Signal<(PeerReadState, PeerReadStateMarker)?, PeerReadStateValidationError> in
switch result {
case let .peerDialogs(dialogs, _, _, _, state):
if let dialog = dialogs.filter({ $0.peerId == peerId }).first {
let apiTopMessage: Int32
let apiReadInboxMaxId: Int32
let apiReadOutboxMaxId: Int32
let apiUnreadCount: Int32
let apiMarkedUnread: Bool
var apiChannelPts: Int32 = 0
switch dialog {
case let .dialog(flags, _, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, _, _, _, pts, _, _, _):
apiTopMessage = topMessage
apiReadInboxMaxId = readInboxMaxId
apiReadOutboxMaxId = readOutboxMaxId
apiUnreadCount = unreadCount
apiMarkedUnread = (flags & (1 << 3)) != 0
if let pts = pts {
apiChannelPts = pts
}
case .dialogFolder:
assertionFailure()
return .fail(.retry)
}
let marker: PeerReadStateMarker
if peerId.namespace == Namespaces.Peer.CloudChannel {
marker = .Channel(apiChannelPts)
} else {
let pts: Int32
switch state {
case let .state(statePts, _, _, _, _):
pts = statePts
}
marker = .Global(pts)
}
return .single((.idBased(maxIncomingReadId: apiReadInboxMaxId, maxOutgoingReadId: apiReadOutboxMaxId, maxKnownId: apiTopMessage, count: apiUnreadCount, markedUnread: apiMarkedUnread), marker))
} else {
return .fail(.retry)
}
}
}
}
}
}
private func localReadStateMarker(transaction: Transaction, peerId: PeerId) -> PeerReadStateMarker? {
if peerId.namespace == Namespaces.Peer.CloudChannel {
if let state = transaction.getPeerChatState(peerId) as? ChannelState {
return .Channel(state.pts)
} else {
return nil
}
} else {
if let state = (transaction.getState() as? AuthorizedAccountState)?.state {
return .Global(state.pts)
} else {
return nil
}
}
}
private func localReadStateMarker(network: Network, postbox: Postbox, peerId: PeerId) -> Signal<PeerReadStateMarker, PeerReadStateValidationError> {
return postbox.transaction { transaction -> PeerReadStateMarker? in
return localReadStateMarker(transaction: transaction, peerId: peerId)
}
|> mapToSignalPromotingError { marker -> Signal<PeerReadStateMarker, PeerReadStateValidationError> in
if let marker = marker {
return .single(marker)
} else {
return .fail(.retry)
}
}
}
enum PeerReadStateValidationError {
case retry
}
private func validatePeerReadState(network: Network, postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId) -> Signal<Never, PeerReadStateValidationError> {
let readStateWithInitialState = dialogReadState(network: network, postbox: postbox, peerId: peerId)
let maybeAppliedReadState = readStateWithInitialState
|> mapToSignal { data -> Signal<Never, PeerReadStateValidationError> in
guard let (readState, _) = data else {
return postbox.transaction { transaction -> Void in
transaction.confirmSynchronizedIncomingReadState(peerId)
}
|> castError(PeerReadStateValidationError.self)
|> ignoreValues
}
return stateManager.addCustomOperation(postbox.transaction { transaction -> PeerReadStateValidationError? in
if let currentReadState = transaction.getCombinedPeerReadState(peerId) {
loop: for (namespace, currentState) in currentReadState.states {
if namespace == Namespaces.Message.Cloud {
switch currentState {
case let .idBased(localMaxIncomingReadId, _, _, _, _):
if case let .idBased(updatedMaxIncomingReadId, _, _, updatedCount, updatedMarkedUnread) = readState {
if updatedCount != 0 || updatedMarkedUnread {
if localMaxIncomingReadId > updatedMaxIncomingReadId {
return .retry
}
}
}
default:
break
}
break loop
}
}
}
var updatedReadState = readState
if case let .idBased(updatedMaxIncomingReadId, updatedMaxOutgoingReadId, updatedMaxKnownId, updatedCount, updatedMarkedUnread) = readState, let readStates = transaction.getPeerReadStates(peerId) {
for (namespace, state) in readStates {
if namespace == Namespaces.Message.Cloud {
switch state {
case let .idBased(_, maxOutgoingReadId, _, _, _):
updatedReadState = .idBased(maxIncomingReadId: updatedMaxIncomingReadId, maxOutgoingReadId: max(updatedMaxOutgoingReadId, maxOutgoingReadId), maxKnownId: updatedMaxKnownId, count: updatedCount, markedUnread: updatedMarkedUnread)
case .indexBased:
break
}
break
}
}
}
transaction.resetIncomingReadStates([peerId: [Namespaces.Message.Cloud: updatedReadState]])
return nil
}
|> mapToSignalPromotingError { error -> Signal<Never, PeerReadStateValidationError> in
if let error = error {
return .fail(error)
} else {
return .complete()
}
})
}
return maybeAppliedReadState
}
private func pushPeerReadState(network: Network, postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId, readState: PeerReadState) -> Signal<PeerReadState, PeerReadStateValidationError> {
if peerId.namespace == Namespaces.Peer.SecretChat {
return inputSecretChat(postbox: postbox, peerId: peerId)
|> mapToSignal { inputPeer -> Signal<PeerReadState, PeerReadStateValidationError> in
switch readState {
case .idBased:
return .single(readState)
case let .indexBased(maxIncomingReadIndex, _, _, _):
return network.request(Api.functions.messages.readEncryptedHistory(peer: inputPeer, maxDate: maxIncomingReadIndex.timestamp))
|> mapError { _ in
return PeerReadStateValidationError.retry
}
|> mapToSignal { _ -> Signal<PeerReadState, PeerReadStateValidationError> in
return .single(readState)
}
}
}
} else {
return inputPeer(postbox: postbox, peerId: peerId)
|> mapToSignal { inputPeer -> Signal<PeerReadState, PeerReadStateValidationError> in
switch inputPeer {
case let .inputPeerChannel(channelId, accessHash):
switch readState {
case let .idBased(maxIncomingReadId, _, _, _, markedUnread):
var pushSignal: Signal<Void, NoError> = network.request(Api.functions.channels.readHistory(channel: Api.InputChannel.inputChannel(channelId: channelId, accessHash: accessHash), maxId: maxIncomingReadId))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
if markedUnread {
pushSignal = pushSignal
|> then(network.request(Api.functions.messages.markDialogUnread(flags: 1 << 0, parentPeer: nil, peer: .inputDialogPeer(peer: inputPeer)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
})
}
return pushSignal
|> mapError { _ -> PeerReadStateValidationError in
}
|> mapToSignal { _ -> Signal<PeerReadState, PeerReadStateValidationError> in
return .complete()
}
|> then(Signal<PeerReadState, PeerReadStateValidationError>.single(readState))
case .indexBased:
return .single(readState)
}
default:
switch readState {
case let .idBased(maxIncomingReadId, _, _, _, markedUnread):
var pushSignal: Signal<Void, NoError> = network.request(Api.functions.messages.readHistory(peer: inputPeer, maxId: maxIncomingReadId))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AffectedMessages?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .affectedMessages(pts, ptsCount):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
}
}
return .complete()
}
if markedUnread {
pushSignal = pushSignal
|> then(network.request(Api.functions.messages.markDialogUnread(flags: 1 << 0, parentPeer: nil, peer: .inputDialogPeer(peer: inputPeer)))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .complete()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
})
}
return pushSignal
|> mapError { _ -> PeerReadStateValidationError in
}
|> mapToSignal { _ -> Signal<PeerReadState, PeerReadStateValidationError> in
return .complete()
}
|> then(Signal<PeerReadState, PeerReadStateValidationError>.single(readState))
case .indexBased:
return .single(readState)
}
}
}
}
}
private func pushPeerReadState(network: Network, postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId) -> Signal<Never, PeerReadStateValidationError> {
let currentReadState = postbox.transaction { transaction -> (MessageId.Namespace, PeerReadState)? in
if let readStates = transaction.getPeerReadStates(peerId) {
for (namespace, readState) in readStates {
if namespace == Namespaces.Message.Cloud || namespace == Namespaces.Message.SecretIncoming {
return (namespace, readState)
}
}
}
return nil
}
let pushedState = currentReadState
|> mapToSignalPromotingError { namespaceAndReadState -> Signal<(MessageId.Namespace, PeerReadState), PeerReadStateValidationError> in
if let (namespace, readState) = namespaceAndReadState {
return pushPeerReadState(network: network, postbox: postbox, stateManager: stateManager, peerId: peerId, readState: readState)
|> map { updatedReadState -> (MessageId.Namespace, PeerReadState) in
return (namespace, updatedReadState)
}
} else {
return .complete()
}
}
let verifiedState = pushedState
|> mapToSignal { namespaceAndReadState -> Signal<Never, PeerReadStateValidationError> in
return stateManager.addCustomOperation(postbox.transaction { transaction -> PeerReadStateValidationError? in
if let readStates = transaction.getPeerReadStates(peerId) {
for (namespace, currentReadState) in readStates where namespace == namespaceAndReadState.0 {
if currentReadState.count == namespaceAndReadState.1.count {
transaction.confirmSynchronizedIncomingReadState(peerId)
return nil
}
}
return .retry
} else {
transaction.confirmSynchronizedIncomingReadState(peerId)
return nil
}
}
|> mapToSignalPromotingError { error -> Signal<Never, PeerReadStateValidationError> in
if let error = error {
return .fail(error)
} else {
return .complete()
}
})
}
return verifiedState
}
func synchronizePeerReadState(network: Network, postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId, push: Bool, validate: Bool) -> Signal<Never, PeerReadStateValidationError> {
var signal: Signal<Never, PeerReadStateValidationError> = .complete()
if push {
signal = signal
|> then(pushPeerReadState(network: network, postbox: postbox, stateManager: stateManager, peerId: peerId))
}
if validate {
signal = signal
|> then(validatePeerReadState(network: network, postbox: postbox, stateManager: stateManager, peerId: peerId))
}
return signal
}
@@ -0,0 +1,63 @@
import Foundation
import Postbox
import SwiftSignalKit
enum RecentlyUsedMediaCategory {
case stickers
}
func addSynchronizeRecentlyUsedMediaOperation(transaction: Transaction, category: RecentlyUsedMediaCategory, operation: SynchronizeRecentlyUsedMediaOperationContent) {
let tag: PeerOperationLogTag
switch category {
case .stickers:
tag = OperationLogTags.SynchronizeRecentlyUsedStickers
}
let peerId = PeerId(0)
var removeOperations: [(SynchronizeRecentlyUsedMediaOperation, Int32)] = []
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in
if let operation = entry.contents as? SynchronizeRecentlyUsedMediaOperation {
if case .sync = operation.content {
removeOperations.append((operation, entry.tagLocalIndex))
return true
} else {
return false
}
} else {
return false
}
})
for (_, topLocalIndex) in removeOperations {
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex)
}
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeRecentlyUsedMediaOperation(content: operation))
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeRecentlyUsedMediaOperation(content: .sync))
}
func addRecentlyUsedSticker(transaction: Transaction, fileReference: FileMediaReference) {
if let resource = fileReference.media.resource as? CloudDocumentMediaResource {
if let entry = CodableEntry(RecentMediaItem(fileReference.media)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStickers, item: OrderedItemListEntry(id: RecentMediaItemId(fileReference.media.fileId).rawValue, contents: entry), removeTailIfCountExceeds: 20)
}
addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .add(id: resource.fileId, accessHash: resource.accessHash, fileReference: fileReference))
}
}
func _internal_removeRecentlyUsedSticker(transaction: Transaction, fileReference: FileMediaReference) {
if let resource = fileReference.media.resource as? CloudDocumentMediaResource {
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStickers, itemId: RecentMediaItemId(fileReference.media.fileId).rawValue)
addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .remove(id: resource.fileId, accessHash: resource.accessHash))
}
}
func _internal_clearRecentlyUsedStickers(transaction: Transaction) {
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudRecentStickers, items: [])
addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .clear)
}
func _internal_clearRecentlyUsedEmoji(transaction: Transaction) {
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.LocalRecentEmoji, items: [])
}
@@ -0,0 +1,99 @@
import Foundation
import Postbox
import SwiftSignalKit
func addSynchronizeSavedGifsOperation(transaction: Transaction, operation: SynchronizeSavedGifsOperationContent) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeSavedGifs
let peerId = PeerId(0)
var topOperation: (SynchronizeSavedGifsOperation, Int32)?
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in
if let operation = entry.contents as? SynchronizeSavedGifsOperation {
topOperation = (operation, entry.tagLocalIndex)
}
return false
})
if let (topOperation, topLocalIndex) = topOperation, case .sync = topOperation.content {
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex)
}
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeSavedGifsOperation(content: operation))
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeSavedGifsOperation(content: .sync))
}
public func getIsGifSaved(transaction: Transaction, mediaId: MediaId) -> Bool {
if transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentGifs, itemId: RecentMediaItemId(mediaId).rawValue) != nil {
return true
}
return false
}
public func addSavedGif(postbox: Postbox, fileReference: FileMediaReference, limit: Int = 200) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if let resource = fileReference.media.resource as? CloudDocumentMediaResource {
if let entry = CodableEntry(RecentMediaItem(fileReference.media)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentGifs, item: OrderedItemListEntry(id: RecentMediaItemId(fileReference.media.fileId).rawValue, contents: entry), removeTailIfCountExceeds: limit)
}
addSynchronizeSavedGifsOperation(transaction: transaction, operation: .add(id: resource.fileId, accessHash: resource.accessHash, fileReference: fileReference))
}
}
}
public func removeSavedGif(postbox: Postbox, mediaId: MediaId) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
if let entry = transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentGifs, itemId: RecentMediaItemId(mediaId).rawValue), let item = entry.contents.get(RecentMediaItem.self) {
let file = item.media
if let resource = file._parse().resource as? CloudDocumentMediaResource {
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentGifs, itemId: entry.id)
addSynchronizeSavedGifsOperation(transaction: transaction, operation: .remove(id: resource.fileId, accessHash: resource.accessHash))
}
}
}
}
public enum SavedGifResult {
case generic
case limitExceeded(Int32, Int32)
}
public func toggleGifSaved(account: Account, fileReference: FileMediaReference, saved: Bool) -> Signal<SavedGifResult, NoError> {
if saved {
return account.postbox.transaction { transaction -> Signal<SavedGifResult, NoError> in
let isPremium = transaction.getPeer(account.peerId)?.isPremium ?? false
let items = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)
let appConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? .defaultValue
let limitsConfiguration = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: false)
let premiumLimitsConfiguration = UserLimitsConfiguration(appConfiguration: appConfiguration, isPremium: true)
let result: SavedGifResult
if isPremium && items.count >= premiumLimitsConfiguration.maxSavedGifCount {
result = .limitExceeded(premiumLimitsConfiguration.maxSavedGifCount, premiumLimitsConfiguration.maxSavedGifCount)
} else if !isPremium && items.count >= limitsConfiguration.maxSavedGifCount {
result = .limitExceeded(limitsConfiguration.maxSavedGifCount, premiumLimitsConfiguration.maxSavedGifCount)
} else {
result = .generic
}
return addSavedGif(postbox: account.postbox, fileReference: fileReference, limit: Int(isPremium ? premiumLimitsConfiguration.maxSavedGifCount : limitsConfiguration.maxSavedGifCount))
|> map { _ -> SavedGifResult in
return .generic
}
|> filter { _ in
return false
}
|> then(
.single(result)
)
}
|> switchToLatest
} else {
return removeSavedSticker(postbox: account.postbox, mediaId: fileReference.media.fileId)
|> map { _ -> SavedGifResult in
return .generic
}
}
}
@@ -0,0 +1,144 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
func addSynchronizeSavedStickersOperation(transaction: Transaction, operation: SynchronizeSavedStickersOperationContent) {
let tag: PeerOperationLogTag = OperationLogTags.SynchronizeSavedStickers
let peerId = PeerId(0)
var topOperation: (SynchronizeSavedStickersOperation, Int32)?
transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in
if let operation = entry.contents as? SynchronizeSavedStickersOperation {
topOperation = (operation, entry.tagLocalIndex)
}
return false
})
if let (topOperation, topLocalIndex) = topOperation, case .sync = topOperation.content {
let _ = transaction.operationLogRemoveEntry(peerId: peerId, tag: tag, tagLocalIndex: topLocalIndex)
}
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeSavedStickersOperation(content: operation))
transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizeSavedStickersOperation(content: .sync))
}
public enum AddSavedStickerError {
case generic
case notFound
}
public func getIsStickerSaved(transaction: Transaction, fileId: MediaId) -> Bool {
if let _ = transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudSavedStickers, itemId: RecentMediaItemId(fileId).rawValue) {
return true
} else{
return false
}
}
public func addSavedSticker(postbox: Postbox, network: Network, file: TelegramMediaFile, limit: Int = 5) -> Signal<Void, AddSavedStickerError> {
return postbox.transaction { transaction -> Signal<Void, AddSavedStickerError> in
for attribute in file.attributes {
if case let .Sticker(_, maybePackReference, _) = attribute {
if let packReference = maybePackReference {
var fetchReference: StickerPackReference?
switch packReference {
case .name:
fetchReference = packReference
case let .id(id, _):
let items = transaction.getItemCollectionItems(collectionId: ItemCollectionId(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id))
var found = false
inner: for item in items {
if let stickerItem = item as? StickerPackItem {
if stickerItem.file.fileId == file.fileId {
let stringRepresentations = stickerItem.getStringRepresentationsOfIndexKeys()
found = true
addSavedSticker(transaction: transaction, file: stickerItem.file._parse(), stringRepresentations: stringRepresentations)
break inner
}
}
}
if !found {
fetchReference = packReference
}
case .animatedEmoji, .animatedEmojiAnimations, .dice, .premiumGifts, .emojiGenericAnimations, .iconStatusEmoji, .iconChannelStatusEmoji, .iconTopicEmoji, .tonGifts:
break
}
if let fetchReference = fetchReference {
return network.request(Api.functions.messages.getStickerSet(stickerset: fetchReference.apiInputStickerSet, hash: 0))
|> mapError { _ -> AddSavedStickerError in
return .generic
}
|> mapToSignal { result -> Signal<Void, AddSavedStickerError> in
var stickerStringRepresentations: [String]?
switch result {
case .stickerSetNotModified:
break
case let .stickerSet(_, packs, _, _):
var stringRepresentationsByFile: [MediaId: [String]] = [:]
for pack in packs {
switch pack {
case let .stickerPack(text, fileIds):
for fileId in fileIds {
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
if stringRepresentationsByFile[mediaId] == nil {
stringRepresentationsByFile[mediaId] = [text]
} else {
stringRepresentationsByFile[mediaId]!.append(text)
}
}
}
}
stickerStringRepresentations = stringRepresentationsByFile[file.fileId]
}
if let stickerStringRepresentations = stickerStringRepresentations {
return postbox.transaction { transaction -> Void in
addSavedSticker(transaction: transaction, file: file, stringRepresentations: stickerStringRepresentations)
} |> mapError { _ -> AddSavedStickerError in }
} else {
return .fail(.notFound)
}
}
}
return .complete()
} else {
return postbox.transaction { transaction -> Void in
addSavedSticker(transaction: transaction, file: file, stringRepresentations: [])
} |> mapError { _ -> AddSavedStickerError in }
}
}
}
return .complete()
} |> mapError { _ -> AddSavedStickerError in } |> switchToLatest
}
public func addSavedSticker(transaction: Transaction, file: TelegramMediaFile, stringRepresentations: [String], limit: Int = 5) {
if let resource = file.resource as? CloudDocumentMediaResource {
if let entry = CodableEntry(SavedStickerItem(file: file, stringRepresentations: stringRepresentations)) {
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudSavedStickers, item: OrderedItemListEntry(id: RecentMediaItemId(file.fileId).rawValue, contents: entry), removeTailIfCountExceeds: limit)
}
addSynchronizeSavedStickersOperation(transaction: transaction, operation: .add(id: resource.fileId, accessHash: resource.accessHash, fileReference: .standalone(media: file)))
}
}
public func removeSavedSticker(transaction: Transaction, mediaId: MediaId) {
if let entry = transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudSavedStickers, itemId: RecentMediaItemId(mediaId).rawValue), let item = entry.contents.get(SavedStickerItem.self) {
if let resource = item.file._parse().resource as? CloudDocumentMediaResource {
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudSavedStickers, itemId: entry.id)
addSynchronizeSavedStickersOperation(transaction: transaction, operation: .remove(id: resource.fileId, accessHash: resource.accessHash))
}
}
}
public func removeSavedSticker(postbox: Postbox, mediaId: MediaId) -> Signal<Void, NoError> {
return postbox.transaction { transaction in
if let entry = transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudSavedStickers, itemId: RecentMediaItemId(mediaId).rawValue), let item = entry.contents.get(SavedStickerItem.self) {
if let resource = item.file._parse().resource as? CloudDocumentMediaResource {
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudSavedStickers, itemId: entry.id)
addSynchronizeSavedStickersOperation(transaction: transaction, operation: .remove(id: resource.fileId, accessHash: resource.accessHash))
}
}
}
}
@@ -0,0 +1,109 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
private final class UnauthorizedUpdateMessageService: NSObject, MTMessageService {
let pipe: ValuePipe<[Api.Update]> = ValuePipe()
var mtProto: MTProto?
override init() {
super.init()
}
func mtProtoWillAdd(_ mtProto: MTProto!) {
self.mtProto = mtProto
}
func mtProtoDidChangeSession(_ mtProto: MTProto!) {
}
func mtProtoServerDidChangeSession(_ mtProto: MTProto!, firstValidMessageId: Int64, otherValidMessageIds: [Any]!) {
}
func putNext(_ updates: [Api.Update]) {
self.pipe.putNext(updates)
}
func mtProto(_ mtProto: MTProto!, receivedMessage message: MTIncomingMessage!, authInfoSelector: MTDatacenterAuthInfoSelector, networkType: Int32) {
if let updates = (message.body as? BoxedMessage)?.body as? Api.Updates {
self.addUpdates(updates)
}
}
func addUpdates(_ updates: Api.Updates) {
switch updates {
case let .updates(updates, _, _, _, _):
self.putNext(updates)
case let .updatesCombined(updates, _, _, _, _, _):
self.putNext(updates)
case let .updateShort(update, _):
self.putNext([update])
case .updateShortChatMessage, .updateShortMessage, .updatesTooLong, .updateShortSentMessage:
break
}
}
}
final class UnauthorizedAccountStateManager {
private let queue = Queue()
private let network: Network
private var updateService: UnauthorizedUpdateMessageService?
private let updateServiceDisposable = MetaDisposable()
private let updateLoginToken: () -> Void
private let updateSentCode: (Api.auth.SentCode) -> Void
private let displayServiceNotification: (String) -> Void
init(
network: Network,
updateLoginToken: @escaping () -> Void,
updateSentCode: @escaping (Api.auth.SentCode) -> Void,
displayServiceNotification: @escaping (String) -> Void
) {
self.network = network
self.updateLoginToken = updateLoginToken
self.updateSentCode = updateSentCode
self.displayServiceNotification = displayServiceNotification
}
deinit {
self.updateServiceDisposable.dispose()
}
func addUpdates(_ updates: Api.Updates) {
self.queue.async {
self.updateService?.addUpdates(updates)
}
}
func reset() {
self.queue.async {
if self.updateService == nil {
self.updateService = UnauthorizedUpdateMessageService()
let updateLoginToken = self.updateLoginToken
let updateSentCode = self.updateSentCode
let displayServiceNotification = self.displayServiceNotification
self.updateServiceDisposable.set(self.updateService!.pipe.signal().start(next: { updates in
for update in updates {
switch update {
case .updateLoginToken:
updateLoginToken()
case let .updateServiceNotification(flags, _, _, message, _, _):
let popup = (flags & (1 << 0)) != 0
if popup {
displayServiceNotification(message)
}
case let .updateSentPhoneCode(sentCode):
updateSentCode(sentCode)
default:
break
}
}
}))
self.network.mtProto.add(self.updateService)
}
}
}
}
@@ -0,0 +1,226 @@
import Foundation
import TelegramApi
import Postbox
enum UpdateGroup {
case withPts(updates: [Api.Update], users: [Api.User], chats: [Api.Chat])
case withQts(updates: [Api.Update], users: [Api.User], chats: [Api.Chat])
case withSeq(updates: [Api.Update], seqRange: (Int32, Int32), date: Int32, users: [Api.User], chats: [Api.Chat])
case withDate(updates: [Api.Update], date: Int32, users: [Api.User], chats: [Api.Chat])
case reset
case updatePts(pts: Int32, ptsCount: Int32)
case updateChannelPts(channelId: Int64, pts: Int32, ptsCount: Int32)
case ensurePeerHasLocalState(id: PeerId)
var updates: [Api.Update] {
switch self {
case let .withPts(updates, _, _):
return updates
case let .withDate(updates, _, _, _):
return updates
case let .withQts(updates, _, _):
return updates
case let .withSeq(updates, _, _, _, _):
return updates
case .reset, .updatePts, .updateChannelPts, .ensurePeerHasLocalState:
return []
}
}
var users: [Api.User] {
switch self {
case let .withPts(_, users, _):
return users
case let .withDate(_, _, users, _):
return users
case let .withQts(_, users, _):
return users
case let .withSeq(_, _, _, users, _):
return users
case .reset, .updatePts, .updateChannelPts, .ensurePeerHasLocalState:
return []
}
}
var chats: [Api.Chat] {
switch self {
case let .withPts(_, _, chats):
return chats
case let .withDate(_, _, _, chats):
return chats
case let .withQts(_, _, chats):
return chats
case let .withSeq(_, _, _, _, chats):
return chats
case .reset, .updatePts, .updateChannelPts, .ensurePeerHasLocalState:
return []
}
}
}
func apiUpdatePtsRange(_ update: Api.Update) -> (Int32, Int32)? {
switch update {
case let .updateDeleteMessages(_, pts, ptsCount):
return (pts, ptsCount)
case let .updateNewMessage(_, pts, ptsCount):
return (pts, ptsCount)
case let .updateReadHistoryInbox(_, _, _, _, _, _, pts, ptsCount):
return (pts, ptsCount)
case let .updateReadHistoryOutbox(_, _, pts, ptsCount):
return (pts, ptsCount)
case let .updateEditMessage(_, pts, ptsCount):
return (pts, ptsCount)
case let .updateReadMessagesContents(_, _, pts, ptsCount, _):
return (pts, ptsCount)
case let .updateWebPage(_, pts, ptsCount):
return (pts, ptsCount)
case let .updateFolderPeers(_, pts, ptsCount):
if ptsCount != 0 {
return (pts, ptsCount)
} else {
return nil
}
case let .updatePinnedMessages(_, _, _, pts, ptsCount):
return (pts, ptsCount)
default:
return nil
}
}
func apiUpdateQtsRange(_ update: Api.Update) -> (Int32, Int32)? {
switch update {
case let .updateNewEncryptedMessage(_, qts):
return (qts, 1)
case _:
return nil
}
}
struct PtsUpdate {
let update: Api.Update?
let ptsRange: (Int32, Int32)
let users: [Api.User]
let chats: [Api.Chat]
}
struct QtsUpdate {
let update: Api.Update
let qtsRange: (Int32, Int32)
let users: [Api.User]
let chats: [Api.Chat]
}
struct SeqUpdates {
let updates: [Api.Update]
let seqRange: (Int32, Int32)
let date: Int32
let users: [Api.User]
let chats: [Api.Chat]
}
func ptsUpdates(_ groups: [UpdateGroup]) -> [PtsUpdate] {
var result: [PtsUpdate] = []
for group in groups {
switch group {
case let .withPts(updates, users, chats):
for update in updates {
if let ptsRange = apiUpdatePtsRange(update) {
result.append(PtsUpdate(update: update, ptsRange: ptsRange, users: users, chats: chats))
}
}
case let .updatePts(pts, ptsCount):
result.append(PtsUpdate(update: nil, ptsRange: (pts, ptsCount), users: [], chats: []))
case _:
break
}
}
result.sort(by: { $0.ptsRange.0 < $1.ptsRange.0 })
return result
}
func qtsUpdates(_ groups: [UpdateGroup]) -> [QtsUpdate] {
var result: [QtsUpdate] = []
for group in groups {
switch group {
case let .withQts(updates, users, chats):
for update in updates {
if let qtsRange = apiUpdateQtsRange(update) {
result.append(QtsUpdate(update: update, qtsRange: qtsRange, users: users, chats: chats))
}
}
break
case _:
break
}
}
result.sort(by: { $0.qtsRange.0 < $1.qtsRange.0 })
return result
}
func seqGroups(_ groups: [UpdateGroup]) -> [SeqUpdates] {
var result: [SeqUpdates] = []
for group in groups {
switch group {
case let .withSeq(updates, seqRange, date, users, chats):
result.append(SeqUpdates(updates: updates, seqRange: seqRange, date: date, users: users, chats: chats))
case _:
break
}
}
return result
}
func dateGroups(_ groups: [UpdateGroup]) -> [UpdateGroup] {
var result: [UpdateGroup] = []
for group in groups {
switch group {
case .withDate:
result.append(group)
case _:
break
}
}
return result
}
func groupUpdates(_ updates: [Api.Update], users: [Api.User], chats: [Api.Chat], date: Int32, seqRange: (Int32, Int32)?) -> [UpdateGroup] {
var updatesWithPts: [Api.Update] = []
var updatesWithQts: [Api.Update] = []
var otherUpdates: [Api.Update] = []
for update in updates {
if let _ = apiUpdatePtsRange(update) {
updatesWithPts.append(update)
} else if let _ = apiUpdateQtsRange(update) {
updatesWithQts.append(update)
} else {
otherUpdates.append(update)
}
}
var groups: [UpdateGroup] = []
if updatesWithPts.count != 0 {
groups.append(.withPts(updates: updatesWithPts, users: users, chats: chats))
}
if updatesWithQts.count != 0 {
groups.append(.withQts(updates: updatesWithQts, users: users, chats: chats))
}
if let seqRange = seqRange {
groups.append(.withSeq(updates: otherUpdates, seqRange: seqRange, date: date, users: users, chats: chats))
} else {
groups.append(.withDate(updates: otherUpdates, date: date, users: users, chats: chats))
}
return groups
}
@@ -0,0 +1,89 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
class UpdateMessageService: NSObject, MTMessageService {
var peerId: PeerId!
let pipe: ValuePipe<[UpdateGroup]> = ValuePipe()
var mtProto: MTProto?
override init() {
super.init()
}
convenience init(peerId: PeerId) {
self.init()
self.peerId = peerId
}
func mtProtoWillAdd(_ mtProto: MTProto!) {
self.mtProto = mtProto
}
func mtProtoDidChangeSession(_ mtProto: MTProto!) {
self.pipe.putNext([.reset])
}
func mtProtoServerDidChangeSession(_ mtProto: MTProto!, firstValidMessageId: Int64, otherValidMessageIds: [Any]!) {
self.pipe.putNext([.reset])
}
func putNext(_ groups: [UpdateGroup]) {
self.pipe.putNext(groups)
}
func mtProto(_ mtProto: MTProto!, receivedMessage message: MTIncomingMessage!, authInfoSelector: MTDatacenterAuthInfoSelector, networkType: Int32) {
if let updates = (message.body as? BoxedMessage)?.body as? Api.Updates {
self.addUpdates(updates)
}
}
func addUpdates(_ updates: Api.Updates) {
switch updates {
case let .updates(updates, users, chats, date, seq):
let groups = groupUpdates(updates, users: users, chats: chats, date: date, seqRange: seq == 0 ? nil : (seq, 1))
if groups.count != 0 {
self.putNext(groups)
}
case let .updatesCombined(updates, users, chats, date, seqStart, seq):
let groups = groupUpdates(updates, users: users, chats: chats, date: date, seqRange: seq == 0 ? nil : (seq, seq - seqStart))
if groups.count != 0 {
self.putNext(groups)
}
case let .updateShort(update, date):
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
if groups.count != 0 {
self.putNext(groups)
}
case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod):
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil)
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
if groups.count != 0 {
self.putNext(groups)
}
case let .updateShortMessage(flags, id, userId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod):
let generatedFromId: Api.Peer
if (Int(flags) & 1 << 1) != 0 {
generatedFromId = Api.Peer.peerUser(userId: self.peerId.id._internalGetInt64Value())
} else {
generatedFromId = Api.Peer.peerUser(userId: userId)
}
let generatedPeerId = Api.Peer.peerUser(userId: userId)
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil)
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
if groups.count != 0 {
self.putNext(groups)
}
case .updatesTooLong:
self.pipe.putNext([.reset])
case let .updateShortSentMessage(_, _, pts, ptsCount, _, _, _, _):
self.pipe.putNext([.updatePts(pts: pts, ptsCount: ptsCount)])
}
}
}
@@ -0,0 +1,647 @@
import Foundation
import Postbox
import TelegramApi
private func collectPreCachedResources(for photo: Api.Photo) -> [(MediaResource, Data)]? {
switch photo {
case let .photo(_, id, accessHash, fileReference, _, sizes, _, dcId):
for size in sizes {
switch size {
case let .photoCachedSize(type, _, _, bytes):
let resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, size: nil, fileReference: fileReference.makeData())
let data = bytes.makeData()
return [(resource, data)]
default:
break
}
}
return nil
case .photoEmpty:
return nil
}
}
private func collectPreCachedResources(for document: Api.Document) -> [(MediaResource, Data)]? {
switch document {
case let .document(_, id, accessHash, fileReference, _, _, _, thumbs, _, dcId, _):
if let thumbs = thumbs {
for thumb in thumbs {
switch thumb {
case let .photoCachedSize(type, _, _, bytes):
let resource = CloudDocumentSizeMediaResource(datacenterId: dcId, documentId: id, accessHash: accessHash, sizeSpec: type, fileReference: fileReference.makeData())
let data = bytes.makeData()
return [(resource, data)]
default:
break
}
}
}
default:
break
}
return nil
}
extension Api.MessageMedia {
var preCachedResources: [(MediaResource, Data)]? {
switch self {
case let .messageMediaPhoto(_, photo, _):
if let photo = photo {
return collectPreCachedResources(for: photo)
} else {
return nil
}
case let .messageMediaDocument(_, document, _, _, _, _):
if let document = document {
return collectPreCachedResources(for: document)
}
return nil
case let .messageMediaWebPage(flags, webPage):
let _ = flags
var result: [(MediaResource, Data)]?
switch webPage {
case let .webPage(_, _, _, _, _, _, _, _, _, photo, _, _, _, _, _, _, document, _, _):
if let photo = photo {
if let photoResult = collectPreCachedResources(for: photo) {
if result == nil {
result = []
}
result!.append(contentsOf: photoResult)
}
}
if let file = document {
if let fileResult = collectPreCachedResources(for: file) {
if result == nil {
result = []
}
result!.append(contentsOf: fileResult)
}
}
default:
break
}
return result
default:
return nil
}
}
var preCachedStories: [StoryId: Api.StoryItem]? {
switch self {
case let .messageMediaStory(_, peerId, id, story):
if let story = story {
return [StoryId(peerId: peerId.peerId, id: id): story]
} else {
return nil
}
default:
return nil
}
}
}
extension Api.Message {
var rawId: Int32 {
switch self {
case let .message(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return id
case let .messageEmpty(_, id, _):
return id
case let .messageService(_, id, _, _, _, _, _, _, _, _):
return id
}
}
func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? {
switch self {
case let .message(_, flags2, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
var namespace = namespace
if (flags2 & (1 << 4)) != 0 {
namespace = Namespaces.Message.ScheduledCloud
}
let peerId: PeerId = messagePeerId.peerId
return MessageId(peerId: peerId, namespace: namespace, id: id)
case let .messageEmpty(_, id, peerId):
if let peerId = peerId {
return MessageId(peerId: peerId.peerId, namespace: Namespaces.Message.Cloud, id: id)
} else {
return nil
}
case let .messageService(_, id, _, chatPeerId, _, _, _, _, _, _):
let peerId: PeerId = chatPeerId.peerId
return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id)
}
}
var peerId: PeerId? {
switch self {
case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
let peerId: PeerId = messagePeerId.peerId
return peerId
case let .messageEmpty(_, _, peerId):
return peerId?.peerId
case let .messageService(_, _, _, chatPeerId, _, _, _, _, _, _):
let peerId: PeerId = chatPeerId.peerId
return peerId
}
}
var timestamp: Int32? {
switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return date
case let .messageService(_, _, _, _, _, _, date, _, _, _):
return date
case .messageEmpty:
return nil
}
}
var preCachedResources: [(MediaResource, Data)]? {
switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return media?.preCachedResources
default:
return nil
}
}
var preCachedStories: [StoryId: Api.StoryItem]? {
switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return media?.preCachedStories
default:
return nil
}
}
}
extension Api.Chat {
var peerId: PeerId {
switch self {
case let .chat(_, id, _, _, _, _, _, _, _, _):
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id))
case let .chatEmpty(id):
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id))
case let .chatForbidden(id, _):
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id))
case let .channel(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id))
case let .channelForbidden(_, id, _, _, _):
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id))
}
}
}
extension Api.User {
var peerId: PeerId {
switch self {
case let .user(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
case let .userEmpty(id):
return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
}
}
}
extension Api.Peer {
var peerId: PeerId {
switch self {
case let .peerChannel(channelId):
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))
case let .peerChat(chatId):
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))
case let .peerUser(userId):
return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
}
}
}
extension Api.Dialog {
var peerId: PeerId? {
switch self {
case let .dialog(_, peer, _, _, _, _, _, _, _, _, _, _, _):
return peer.peerId
case .dialogFolder:
return nil
}
}
}
extension Api.Update {
var rawMessageId: Int32? {
switch self {
case let .updateMessageID(id, _):
return id
case let .updateNewMessage(message, _, _):
return message.rawId
case let .updateNewChannelMessage(message, _, _):
return message.rawId
default:
return nil
}
}
var updatedRawMessageId: (Int64, Int32)? {
switch self {
case let .updateMessageID(id, randomId):
return (randomId, id)
default:
return nil
}
}
var messageId: MessageId? {
switch self {
case let .updateNewMessage(message, _, _):
return message.id()
case let .updateNewChannelMessage(message, _, _):
return message.id()
default:
return nil
}
}
var message: Api.Message? {
switch self {
case let .updateNewMessage(message, _, _):
return message
case let .updateNewChannelMessage(message, _, _):
return message
case let .updateEditMessage(message, _, _):
return message
case let .updateEditChannelMessage(message, _, _):
return message
case let .updateNewScheduledMessage(message):
return message
case let .updateQuickReplyMessage(message):
return message
default:
return nil
}
}
var peerIds: [PeerId] {
switch self {
case let .updateChannel(channelId):
return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))]
case let .updateChat(chatId):
return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))]
case let .updateChannelTooLong(_, channelId, _):
return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))]
case let .updateChatParticipantAdd(chatId, userId, inviterId, _, _):
return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)), PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(inviterId))]
case let .updateChatParticipantAdmin(chatId, userId, _, _):
return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)), PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))]
case let .updateChatParticipantDelete(chatId, userId, _):
return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)), PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))]
case let .updateChatParticipants(participants):
switch participants {
case let .chatParticipants(chatId, _, _):
return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))]
case let .chatParticipantsForbidden(_, chatId, _):
return [PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))]
}
case let .updateDeleteChannelMessages(channelId, _, _, _):
return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))]
case let .updatePinnedChannelMessages(_, channelId, _, _, _):
return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))]
case let .updateNewChannelMessage(message, _, _):
return apiMessagePeerIds(message)
case let .updateEditChannelMessage(message, _, _):
return apiMessagePeerIds(message)
case let .updateChannelWebPage(channelId, _, _, _):
return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))]
case let .updateNewMessage(message, _, _):
return apiMessagePeerIds(message)
case let .updateEditMessage(message, _, _):
return apiMessagePeerIds(message)
case let .updateReadChannelInbox(_, _, channelId, _, _, _):
return [PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))]
case let .updateNotifySettings(peer, _):
switch peer {
case let .notifyPeer(peer):
return [peer.peerId]
default:
return []
}
case let .updateUserName(userId, _, _, _):
return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))]
case let .updateUserPhone(userId, _):
return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))]
case let .updateServiceNotification(_, inboxDate, _, _, _, _):
if let _ = inboxDate {
return [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000))]
} else {
return []
}
case let .updateDraftMessage(_, peer, _, _, _):
return [peer.peerId]
case let .updateNewScheduledMessage(message):
return apiMessagePeerIds(message)
case let .updateQuickReplyMessage(message):
return apiMessagePeerIds(message)
default:
return []
}
}
var associatedMessageIds: (replyIds: ReferencedReplyMessageIds, generalIds: [MessageId])? {
switch self {
case let .updateNewMessage(message, _, _):
return apiMessageAssociatedMessageIds(message)
case let .updateNewChannelMessage(message, _, _):
return apiMessageAssociatedMessageIds(message)
case let .updateEditChannelMessage(message, _, _):
return apiMessageAssociatedMessageIds(message)
case let .updateNewScheduledMessage(message):
return apiMessageAssociatedMessageIds(message)
case let .updateQuickReplyMessage(message):
return apiMessageAssociatedMessageIds(message)
default:
break
}
return nil
}
var channelPts: Int32? {
switch self {
case let .updateNewChannelMessage(_, pts, _):
return pts
case let .updateEditChannelMessage(_, pts, _):
return pts
default:
return nil
}
}
}
extension Api.Updates {
var allUpdates: [Api.Update] {
switch self {
case let .updates(updates, _, _, _, _):
return updates
case let .updatesCombined(updates, _, _, _, _, _):
return updates
case let .updateShort(update, _):
return [update]
default:
return []
}
}
}
extension Api.Updates {
var rawMessageIds: [Int32] {
switch self {
case let .updates(updates, _, _, _, _):
var result: [Int32] = []
for update in updates {
if let id = update.rawMessageId {
result.append(id)
}
}
return result
case let .updatesCombined(updates, _, _, _, _, _):
var result: [Int32] = []
for update in updates {
if let id = update.rawMessageId {
result.append(id)
}
}
return result
case let .updateShort(update, _):
if let id = update.rawMessageId {
return [id]
} else {
return []
}
case let .updateShortSentMessage(_, id, _, _, _, _, _, _):
return [id]
case .updatesTooLong:
return []
case let .updateShortMessage(_, id, _, _, _, _, _, _, _, _, _, _):
return [id]
case let .updateShortChatMessage(_, id, _, _, _, _, _, _, _, _, _, _, _):
return [id]
}
}
var messageIds: [MessageId] {
switch self {
case let .updates(updates, _, _, _, _):
var result: [MessageId] = []
for update in updates {
if let id = update.messageId {
result.append(id)
}
}
return result
case let .updatesCombined(updates, _, _, _, _, _):
var result: [MessageId] = []
for update in updates {
if let id = update.messageId {
result.append(id)
}
}
return result
case let .updateShort(update, _):
if let id = update.messageId {
return [id]
} else {
return []
}
case .updateShortSentMessage:
return []
case .updatesTooLong:
return []
case let .updateShortMessage(_, id, userId, _, _, _, _, _, _, _, _, _):
return [MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), namespace: Namespaces.Message.Cloud, id: id)]
case let .updateShortChatMessage(_, id, _, chatId, _, _, _, _, _, _, _, _, _):
return [MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId)), namespace: Namespaces.Message.Cloud, id: id)]
}
}
var updatedRawMessageIds: [Int64: Int32] {
switch self {
case let .updates(updates, _, _, _, _):
var result: [Int64: Int32] = [:]
for update in updates {
if let (randomId, id) = update.updatedRawMessageId {
result[randomId] = id
}
}
return result
case let .updatesCombined(updates, _, _, _, _, _):
var result: [Int64: Int32] = [:]
for update in updates {
if let (randomId, id) = update.updatedRawMessageId {
result[randomId] = id
}
}
return result
case let .updateShort(update, _):
if let (randomId, id) = update.updatedRawMessageId {
return [randomId: id]
} else {
return [:]
}
case .updateShortSentMessage:
return [:]
case .updatesTooLong:
return [:]
case .updateShortMessage:
return [:]
case .updateShortChatMessage:
return [:]
}
}
}
extension Api.Updates {
var users: [Api.User] {
switch self {
case let .updates(_, users, _, _, _):
return users
case let .updatesCombined(_, users, _, _, _, _):
return users
default:
return []
}
}
var messages: [Api.Message] {
switch self {
case let .updates(updates, _, _, _, _):
var result: [Api.Message] = []
for update in updates {
if let message = update.message {
result.append(message)
}
}
return result
case let .updatesCombined(updates, _, _, _, _, _):
var result: [Api.Message] = []
for update in updates {
if let message = update.message {
result.append(message)
}
}
return result
case let .updateShort(update, _):
if let message = update.message {
return [message]
} else {
return []
}
default:
return []
}
}
var channelPts: Int32? {
switch self {
case let .updates(updates, _, _, _, _):
var result: Int32?
for update in updates {
if let channelPts = update.channelPts {
if result == nil || channelPts > result! {
result = channelPts
}
}
}
return result
case let .updatesCombined(updates, _, _, _, _, _):
var result: Int32?
for update in updates {
if let channelPts = update.channelPts {
if result == nil || channelPts > result! {
result = channelPts
}
}
}
return result
case let .updateShort(update, _):
if let channelPts = update.channelPts {
return channelPts
} else {
return nil
}
default:
return nil
}
}
}
extension Api.Updates {
var chats: [Api.Chat] {
switch self {
case let .updates(_, _, chats, _, _):
var result: [Api.Chat] = []
for chat in chats {
result.append(chat)
}
return result
case let .updatesCombined(_, _, chats, _, _, _):
var result: [Api.Chat] = []
for chat in chats {
result.append(chat)
}
return result
default:
return []
}
}
}
extension Api.EncryptedChat {
var peerId: PeerId {
switch self {
case let .encryptedChat(id, _, _, _, _, _, _):
return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(id)))
case let .encryptedChatDiscarded(_, id):
return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(id)))
case let .encryptedChatEmpty(id):
return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(id)))
case let .encryptedChatRequested(_, _, id, _, _, _, _, _):
return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(id)))
case let .encryptedChatWaiting(id, _, _, _, _):
return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(id)))
}
}
}
extension Api.EncryptedMessage {
var peerId: PeerId {
switch self {
case let .encryptedMessage(_, chatId, _, _, _):
return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(chatId)))
case let .encryptedMessageService(_, chatId, _, _):
return PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(chatId)))
}
}
}
extension Api.InputMedia {
func withUpdatedStickers(_ stickers: [Api.InputDocument]?) -> Api.InputMedia {
switch self {
case let .inputMediaUploadedDocument(flags, file, thumb, mimeType, attributes, _, videoCover, videoTimestamp, ttlSeconds):
var flags = flags
var attributes = attributes
if let _ = stickers {
flags |= (1 << 0)
attributes.append(.documentAttributeHasStickers)
}
return .inputMediaUploadedDocument(flags: flags, file: file, thumb: thumb, mimeType: mimeType, attributes: attributes, stickers: stickers, videoCover: videoCover, videoTimestamp: videoTimestamp, ttlSeconds: ttlSeconds)
case let .inputMediaUploadedPhoto(flags, file, _, ttlSeconds):
var flags = flags
if let _ = stickers {
flags |= (1 << 0)
}
return .inputMediaUploadedPhoto(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds)
default:
return self
}
}
}
@@ -0,0 +1,176 @@
import Postbox
import SwiftSignalKit
public struct UserLimitsConfiguration: Equatable {
public var maxPinnedChatCount: Int32
public var maxPinnedSavedChatCount: Int32
public var maxArchivedPinnedChatCount: Int32
public var maxChannelsCount: Int32
public var maxPublicLinksCount: Int32
public var maxSavedGifCount: Int32
public var maxFavedStickerCount: Int32
public var maxFoldersCount: Int32
public var maxFolderChatsCount: Int32
public var maxCaptionLength: Int32
public var maxUploadFileParts: Int32
public var maxAboutLength: Int32
public var maxAnimatedEmojisInText: Int32
public var maxReactionsPerMessage: Int32
public var maxSharedFolderInviteLinks: Int32
public var maxSharedFolderJoin: Int32
public var maxStoryCaptionLength: Int32
public var maxExpiringStoriesCount: Int32
public var maxStoriesWeeklyCount: Int32
public var maxStoriesMonthlyCount: Int32
public var maxStoriesSuggestedReactions: Int32
public var maxStoriesLinksCount: Int32
public var maxGiveawayChannelsCount: Int32
public var maxGiveawayCountriesCount: Int32
public var maxGiveawayPeriodSeconds: Int32
public var maxChannelRecommendationsCount: Int32
public var maxConferenceParticipantCount: Int32
public static var defaultValue: UserLimitsConfiguration {
return UserLimitsConfiguration(
maxPinnedChatCount: 5,
maxPinnedSavedChatCount: 5,
maxArchivedPinnedChatCount: 100,
maxChannelsCount: 500,
maxPublicLinksCount: 10,
maxSavedGifCount: 200,
maxFavedStickerCount: 5,
maxFoldersCount: 10,
maxFolderChatsCount: 100,
maxCaptionLength: 1024,
maxUploadFileParts: 4000,
maxAboutLength: 70,
maxAnimatedEmojisInText: 10,
maxReactionsPerMessage: 1,
maxSharedFolderInviteLinks: 3,
maxSharedFolderJoin: 2,
maxStoryCaptionLength: 200,
maxExpiringStoriesCount: 3,
maxStoriesWeeklyCount: 7,
maxStoriesMonthlyCount: 30,
maxStoriesSuggestedReactions: 1,
maxStoriesLinksCount: 3,
maxGiveawayChannelsCount: 10,
maxGiveawayCountriesCount: 10,
maxGiveawayPeriodSeconds: 86400 * 31,
maxChannelRecommendationsCount: 10,
maxConferenceParticipantCount: 100
)
}
public init(
maxPinnedChatCount: Int32,
maxPinnedSavedChatCount: Int32,
maxArchivedPinnedChatCount: Int32,
maxChannelsCount: Int32,
maxPublicLinksCount: Int32,
maxSavedGifCount: Int32,
maxFavedStickerCount: Int32,
maxFoldersCount: Int32,
maxFolderChatsCount: Int32,
maxCaptionLength: Int32,
maxUploadFileParts: Int32,
maxAboutLength: Int32,
maxAnimatedEmojisInText: Int32,
maxReactionsPerMessage: Int32,
maxSharedFolderInviteLinks: Int32,
maxSharedFolderJoin: Int32,
maxStoryCaptionLength: Int32,
maxExpiringStoriesCount: Int32,
maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32,
maxStoriesLinksCount: Int32,
maxGiveawayChannelsCount: Int32,
maxGiveawayCountriesCount: Int32,
maxGiveawayPeriodSeconds: Int32,
maxChannelRecommendationsCount: Int32,
maxConferenceParticipantCount: Int32
) {
self.maxPinnedChatCount = maxPinnedChatCount
self.maxPinnedSavedChatCount = maxPinnedSavedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
self.maxChannelsCount = maxChannelsCount
self.maxPublicLinksCount = maxPublicLinksCount
self.maxSavedGifCount = maxSavedGifCount
self.maxFavedStickerCount = maxFavedStickerCount
self.maxFoldersCount = maxFoldersCount
self.maxFolderChatsCount = maxFolderChatsCount
self.maxCaptionLength = maxCaptionLength
self.maxUploadFileParts = maxUploadFileParts
self.maxAboutLength = maxAboutLength
self.maxAnimatedEmojisInText = maxAnimatedEmojisInText
self.maxReactionsPerMessage = maxReactionsPerMessage
self.maxSharedFolderInviteLinks = maxSharedFolderInviteLinks
self.maxSharedFolderJoin = maxSharedFolderJoin
self.maxStoryCaptionLength = maxStoryCaptionLength
self.maxExpiringStoriesCount = maxExpiringStoriesCount
self.maxStoriesWeeklyCount = maxStoriesWeeklyCount
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
self.maxStoriesLinksCount = maxStoriesLinksCount
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds
self.maxChannelRecommendationsCount = maxChannelRecommendationsCount
self.maxConferenceParticipantCount = maxConferenceParticipantCount
}
}
extension UserLimitsConfiguration {
init(appConfiguration: AppConfiguration, isPremium: Bool) {
let keySuffix = isPremium ? "_premium" : "_default"
var defaultValue = UserLimitsConfiguration.defaultValue
if isPremium {
defaultValue.maxPinnedSavedChatCount = 100
}
func getValue(_ key: String, orElse defaultValue: Int32) -> Int32 {
if let value = appConfiguration.data?[key + keySuffix] as? Double {
return Int32(value)
} else {
return defaultValue
}
}
func getGeneralValue(_ key: String, orElse defaultValue: Int32) -> Int32 {
if let value = appConfiguration.data?[key] as? Double {
return Int32(value)
} else {
return defaultValue
}
}
self.maxPinnedChatCount = getValue("dialogs_pinned_limit", orElse: defaultValue.maxPinnedChatCount)
self.maxPinnedSavedChatCount = getValue("saved_dialogs_pinned_limit", orElse: defaultValue.maxPinnedSavedChatCount)
self.maxArchivedPinnedChatCount = getValue("dialogs_folder_pinned_limit", orElse: defaultValue.maxArchivedPinnedChatCount)
self.maxChannelsCount = getValue("channels_limit", orElse: defaultValue.maxChannelsCount)
self.maxPublicLinksCount = getValue("channels_public_limit", orElse: defaultValue.maxPublicLinksCount)
self.maxSavedGifCount = getValue("saved_gifs_limit", orElse: defaultValue.maxSavedGifCount)
self.maxFavedStickerCount = getValue("stickers_faved_limit", orElse: defaultValue.maxFavedStickerCount)
self.maxFoldersCount = getValue("dialog_filters_limit", orElse: defaultValue.maxFoldersCount)
self.maxFolderChatsCount = getValue("dialog_filters_chats_limit", orElse: defaultValue.maxFolderChatsCount)
self.maxCaptionLength = getValue("caption_length_limit", orElse: defaultValue.maxCaptionLength)
self.maxUploadFileParts = getValue("upload_max_fileparts", orElse: defaultValue.maxUploadFileParts)
self.maxAboutLength = getValue("about_length_limit", orElse: defaultValue.maxAboutLength)
self.maxAnimatedEmojisInText = getGeneralValue("message_animated_emoji_max", orElse: defaultValue.maxAnimatedEmojisInText)
self.maxReactionsPerMessage = getValue("reactions_user_max", orElse: 1)
self.maxSharedFolderInviteLinks = getValue("chatlist_invites_limit", orElse: isPremium ? 100 : 3)
self.maxSharedFolderJoin = getValue("chatlists_joined_limit", orElse: isPremium ? 100 : 2)
self.maxStoryCaptionLength = getValue("story_caption_length_limit", orElse: defaultValue.maxStoryCaptionLength)
self.maxExpiringStoriesCount = getValue("story_expiring_limit", orElse: defaultValue.maxExpiringStoriesCount)
self.maxStoriesWeeklyCount = getValue("stories_sent_weekly_limit", orElse: defaultValue.maxStoriesWeeklyCount)
self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount)
self.maxStoriesSuggestedReactions = getValue("stories_suggested_reactions_limit", orElse: defaultValue.maxStoriesMonthlyCount)
self.maxStoriesLinksCount = getGeneralValue("stories_area_url_max", orElse: defaultValue.maxStoriesLinksCount)
self.maxGiveawayChannelsCount = getGeneralValue("giveaway_add_peers_max", orElse: defaultValue.maxGiveawayChannelsCount)
self.maxGiveawayCountriesCount = getGeneralValue("giveaway_countries_max", orElse: defaultValue.maxGiveawayCountriesCount)
self.maxGiveawayPeriodSeconds = getGeneralValue("giveaway_period_max", orElse: defaultValue.maxGiveawayPeriodSeconds)
self.maxChannelRecommendationsCount = getValue("recommended_channels_limit", orElse: defaultValue.maxChannelRecommendationsCount)
self.maxConferenceParticipantCount = getGeneralValue("conference_call_size_limit", orElse: defaultValue.maxConferenceParticipantCount)
}
}